From 51bc02ecb9bc53cfeee6e81b3aff4992561f6759 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sun, 11 Jul 2021 19:02:13 +0000 Subject: [PATCH 001/130] Added package store settings. --- custom/conf/app.example.ini | 10 ++++++++++ modules/setting/repository.go | 5 +++++ modules/storage/storage.go | 15 ++++++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 251ec7a80e1cd..6186f7c638332 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2085,6 +2085,16 @@ PATH = ;[lfs] ;STORAGE_TYPE = local +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; settings for packages, will override storage setting +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[storage.packages] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; storage type +;STORAGE_TYPE = local + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; customize storage diff --git a/modules/setting/repository.go b/modules/setting/repository.go index c2a6357d94622..63230468a6e5b 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -255,6 +255,10 @@ var ( RepoArchive = struct { Storage }{} + + Packages = struct { + Storage + }{} ) func newRepository() { @@ -334,4 +338,5 @@ func newRepository() { } RepoArchive.Storage = getStorage("repo-archive", "", nil) + Packages.Storage = getStorage("packages", "", nil) } diff --git a/modules/storage/storage.go b/modules/storage/storage.go index 2fe14b41ef0ec..58b953745f075 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -117,6 +117,9 @@ var ( // RepoArchives represents repository archives storage RepoArchives ObjectStorage + + // Packages represents packages storage + Packages ObjectStorage ) // Init init the stoarge @@ -137,7 +140,11 @@ func Init() error { return err } - return initRepoArchives() + if err := initRepoArchives(); err != nil { + return err + } + + return initPackages() } // NewStorage takes a storage type and some config and returns an ObjectStorage or an error @@ -182,3 +189,9 @@ func initRepoArchives() (err error) { RepoArchives, err = NewStorage(setting.RepoArchive.Storage.Type, &setting.RepoArchive.Storage) return } + +func initPackages() (err error) { + log.Info("Initialising Packages storage with type: %s", setting.Packages.Storage.Type) + Packages, err = NewStorage(setting.Packages.Storage.Type, &setting.Packages.Storage) + return +} From 48c2fafe62fc985bcab83d8767d99f80d56db521 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sun, 11 Jul 2021 19:30:23 +0000 Subject: [PATCH 002/130] Added models. --- models/migrations/migrations.go | 2 + models/migrations/v188.go | 44 +++++++++ models/models.go | 2 + models/package.go | 158 ++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+) create mode 100644 models/migrations/v188.go create mode 100644 models/package.go diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index de60d89bbe781..d9a51314ea616 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -325,6 +325,8 @@ var migrations = []Migration{ NewMigration("Create protected tag table", createProtectedTagTable), // v187 -> v188 NewMigration("Drop unneeded webhook related columns", dropWebhookColumns), + // v188 -> v189 + NewMigration("Add package tables", addPackageTables), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v188.go b/models/migrations/v188.go new file mode 100644 index 0000000000000..4f41b54f9f35b --- /dev/null +++ b/models/migrations/v188.go @@ -0,0 +1,44 @@ +// Copyright 2021 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 migrations + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func addPackageTables(x *xorm.Engine) error { + type Package struct { + ID int64 `xorm:"pk autoincr"` + RepositoryID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + CreatorID int64 + Type int `xorm:"UNIQUE(s) INDEX NOT NULL"` + Name string + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + Version string `xorm:"UNIQUE(s) INDEX NOT NULL"` + MetaData interface{} `xorm:"TEXT JSON"` + + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` + } + + if err := x.Sync2(new(Package)); err != nil { + return err + } + + type PackageFile struct { + ID int64 `xorm:"pk autoincr"` + PackageID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + Size int64 + Name string `xorm:"UNIQUE(s) NOT NULL"` + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` + } + + return x.Sync2(new(PackageFile)) +} diff --git a/models/models.go b/models/models.go index 610933d3270bd..c2e0a3df3b7bc 100644 --- a/models/models.go +++ b/models/models.go @@ -138,6 +138,8 @@ func init() { new(PushMirror), new(RepoArchiver), new(ProtectedTag), + new(Package), + new(PackageFile), ) gonicNames := []string{"SSL", "UID"} diff --git a/models/package.go b/models/package.go new file mode 100644 index 0000000000000..e6572682e3f29 --- /dev/null +++ b/models/package.go @@ -0,0 +1,158 @@ +// Copyright 2021 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 models + +import ( + "errors" + "strings" + + "code.gitea.io/gitea/modules/timeutil" +) + +// PackageType +type PackageType int + +// Note: new type must append to the end of list to maintain compatibility. +const ( + PackageGeneric PackageType = iota +) + +var ( + // ErrDuplicatePackage indicates a duplicated package error + ErrDuplicatePackage = errors.New("Package does exist already") + // ErrPackageNotExist indicates a package not exist error + ErrPackageNotExist = errors.New("Package does not exist") +) + +// Package represents a package +type Package struct { + ID int64 `xorm:"pk autoincr"` + RepositoryID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + CreatorID int64 + Type PackageType `xorm:"UNIQUE(s) INDEX NOT NULL"` + Name string + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + Version string `xorm:"UNIQUE(s) INDEX NOT NULL"` + MetaData interface{} `xorm:"TEXT JSON"` + + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` +} + +// PackageFile represents files associated with a package +type PackageFile struct { + ID int64 `xorm:"pk autoincr"` + PackageID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + Size int64 + Name string `xorm:"UNIQUE(s) NOT NULL"` + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` +} + +// GetFiles loads all files associated with the package +func (p *Package) GetFiles() ([]*PackageFile, error) { + packageFiles := make([]*PackageFile, 0, 10) + return packageFiles, x.Where("package_id = ?", p.ID).Find(&packageFiles) +} + +// TryInsertPackage inserts a package +// If a package already exists ErrDuplicatePackage is returned +func TryInsertPackage(p *Package) error { + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + has, err := sess.Get(p) + if err != nil { + return err + } + + if has { + return ErrDuplicatePackage + } + + if _, err = sess.Insert(p); err != nil { + return err + } + + return sess.Commit() +} + +// DeletePackageByID deletes a package and its files by ID +func DeletePackageByID(packageID int64) error { + if err := DeletePackageFilesByPackageID(packageID); err != nil { + return err + } + + _, err := x.ID(packageID).Delete(&Package{}) + return err +} + +// DeletePackagesByRepositoryID deletes all packages of a repository +func DeletePackagesByRepositoryID(repositoryID int64) error { + packages, err := GetPackagesByRepositoryID(repositoryID) + if err != nil { + return err + } + + for _, p := range packages { + if err := DeletePackageByID(p.ID); err != nil { + return err + } + } + + return nil +} + +// GetPackagesByRepositoryID returns all packages of a repository +func GetPackagesByRepositoryID(repositoryID int64) ([]*Package, error) { + packages := make([]*Package, 0, 10) + return packages, x.Where("repository_id = ?", repositoryID).Find(&packages) +} + +// GetPackagesByName gets all repository packages with the specific name +func GetPackagesByName(repositoryID int64, packageType PackageType, packageName string) ([]*Package, error) { + packages := make([]*Package, 0, 10) + return packages, x.Where("repository_id = ? AND type = ? AND lower_name = ?", repositoryID, packageType, strings.ToLower(packageName)).Find(&packages) +} + +// GetPackageByNameAndVersion gets a repository package by name and version +func GetPackageByNameAndVersion(repositoryID int64, packageType PackageType, packageName, packageVersion string) (*Package, error) { + p := &Package{ + RepositoryID: repositoryID, + Type: packageType, + LowerName: strings.ToLower(packageName), + Version: packageVersion, + } + has, err := x.Get(p) + if err != nil { + return nil, err + } else if !has { + return nil, ErrPackageNotExist + } + return p, nil +} + +// InsertPackageFile inserts a package file +func InsertPackageFile(pf *PackageFile) error { + _, err := x.Insert(pf) + return err +} + +// DeletePackageFileByID deletes a package file +func DeletePackageFileByID(fileID int64) error { + _, err := x.ID(fileID).Delete(&PackageFile{}) + return err +} + +// DeletePackageFilesByPackageID deletes all files associated with the package +func DeletePackageFilesByPackageID(packageID int64) error { + _, err := x.Where("package_id = ?", packageID).Delete(&PackageFile{}) + return err +} From 3e801f9ec5e86da36210d7932e6bc581ca1802f9 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sun, 11 Jul 2021 19:50:17 +0000 Subject: [PATCH 003/130] Added generic package registry. --- models/package.go | 2 +- modules/packages/content_store.go | 43 +++++ modules/util/filebuffer/file_backed_buffer.go | 88 ++++++++++ routers/api/v1/api.go | 10 ++ routers/api/v1/packages/generic/generic.go | 152 ++++++++++++++++++ services/packages/packages.go | 95 +++++++++++ 6 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 modules/packages/content_store.go create mode 100644 modules/util/filebuffer/file_backed_buffer.go create mode 100644 routers/api/v1/packages/generic/generic.go create mode 100644 services/packages/packages.go diff --git a/models/package.go b/models/package.go index e6572682e3f29..351a78fd25979 100644 --- a/models/package.go +++ b/models/package.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" ) -// PackageType +// PackageType specifies the different package types type PackageType int // Note: new type must append to the end of list to maintain compatibility. diff --git a/modules/packages/content_store.go b/modules/packages/content_store.go new file mode 100644 index 0000000000000..5c22c8eaf5119 --- /dev/null +++ b/modules/packages/content_store.go @@ -0,0 +1,43 @@ +// Copyright 2021 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 packages + +import ( + "fmt" + "io" + + "code.gitea.io/gitea/modules/storage" +) + +// ContentStore is a wrapper around ObjectStorage +type ContentStore struct { + store storage.ObjectStorage +} + +// NewContentStore creates the default package store +func NewContentStore() *ContentStore { + contentStore := &ContentStore{storage.Packages} + return contentStore +} + +// Get get the package file content +func (s *ContentStore) Get(packageFileID int64) (storage.Object, error) { + return s.store.Open(toRelativePath(packageFileID)) +} + +// Save stores the package file content +func (s *ContentStore) Save(packageFileID int64, r io.Reader, size int64) error { + _, err := s.store.Save(toRelativePath(packageFileID), r, size) + return err +} + +// Delete deletes the package file content +func (s *ContentStore) Delete(packageFileID int64) error { + return s.store.Delete(toRelativePath(packageFileID)) +} + +func toRelativePath(packageFileID int64) string { + return fmt.Sprintf("%d/package", packageFileID) +} diff --git a/modules/util/filebuffer/file_backed_buffer.go b/modules/util/filebuffer/file_backed_buffer.go new file mode 100644 index 0000000000000..fca4aa3f0f8aa --- /dev/null +++ b/modules/util/filebuffer/file_backed_buffer.go @@ -0,0 +1,88 @@ +// Copyright 2021 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 filebuffer + +import ( + "bytes" + "errors" + "io" + "io/ioutil" + "math" + "os" +) + +var ( + // ErrInvalidMemorySize occurs if the memory size is not in a valid range + ErrInvalidMemorySize = errors.New("Memory size must be greater 0 and lower math.MaxInt32") +) + +// FileBackedBuffer implements io.ReadSeekCloser +type FileBackedBuffer struct { + size int64 + buffer *bytes.Reader + tmpFile *os.File +} + +// CreateFromReader creates a file backed buffer which uses a buffer with a fixed memory size. +// If more data is available a temporary file is used to store the data. +func CreateFromReader(r io.Reader, maxMemorySize int) (*FileBackedBuffer, error) { + if maxMemorySize < 0 || maxMemorySize == math.MaxInt32 { + return nil, ErrInvalidMemorySize + } + + var buf bytes.Buffer + n, err := io.CopyN(&buf, r, int64(maxMemorySize+1)) + if err == io.EOF { + return &FileBackedBuffer{ + size: n, + buffer: bytes.NewReader(buf.Bytes()), + }, nil + } + file, err := ioutil.TempFile("", "buffer-") + if err != nil { + return nil, err + } + + n, err = io.Copy(file, io.MultiReader(&buf, r)) + if err != nil { + return nil, err + } + + return &FileBackedBuffer{ + size: n, + tmpFile: file, + }, nil +} + +// Size returns the byte size of the buffered data +func (b *FileBackedBuffer) Size() int64 { + return b.size +} + +// Read implements io.Reader +func (b *FileBackedBuffer) Read(p []byte) (int, error) { + if b.tmpFile != nil { + return b.tmpFile.Read(p) + } + return b.buffer.Read(p) +} + +// Seek implements io.Seeker +func (b *FileBackedBuffer) Seek(offset int64, whence int) (int64, error) { + if b.tmpFile != nil { + return b.tmpFile.Seek(offset, whence) + } + return b.buffer.Seek(offset, whence) +} + +// Close implements io.Closer +func (b *FileBackedBuffer) Close() error { + if b.tmpFile != nil { + err := b.tmpFile.Close() + os.Remove(b.tmpFile.Name()) + return err + } + return nil +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index b4f14bf2d1e03..e860443c05f3f 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -79,6 +79,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/misc" "code.gitea.io/gitea/routers/api/v1/notify" "code.gitea.io/gitea/routers/api/v1/org" + "code.gitea.io/gitea/routers/api/v1/packages/generic" "code.gitea.io/gitea/routers/api/v1/repo" "code.gitea.io/gitea/routers/api/v1/settings" _ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation @@ -970,6 +971,15 @@ func Routes() *web.Route { }, reqAnyRepoReader()) m.Get("/issue_templates", context.ReferencesGitRepo(false), repo.GetIssueTemplates) m.Get("/languages", reqRepoReader(models.UnitTypeCode), repo.GetLanguages) + m.Group("/packages", func() { + m.Group("/generic", func() { + m.Group("/{packagename}/{packageversion}/{filename}", func() { + m.Get("", generic.DownloadPackage) + m.Put("" /*reqRepoWriter(models.UnitTypePackage),*/, generic.UploadPackage) + m.Delete("" /*reqRepoWriter(models.UnitTypePackage),*/, generic.DeletePackage) + }) + }) + }, reqToken()) }, repoAssignment()) }) diff --git a/routers/api/v1/packages/generic/generic.go b/routers/api/v1/packages/generic/generic.go new file mode 100644 index 0000000000000..c713fa7153064 --- /dev/null +++ b/routers/api/v1/packages/generic/generic.go @@ -0,0 +1,152 @@ +// Copyright 2021 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 generic + +import ( + "errors" + "fmt" + "io" + "net/http" + "regexp" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/packages" + "code.gitea.io/gitea/modules/util/filebuffer" + + package_service "code.gitea.io/gitea/services/packages" +) + +var packageNameRegex = regexp.MustCompile(`\A[A-Za-z0-9\.\_\-\+]+\z`) +var packageVersionRegex = regexp.MustCompile(`\A(?:\.?[\w\+-]+\.?)+\z`) +var filenameRegex = packageNameRegex + +// DownloadPackage serves the specific generic package. +func DownloadPackage(ctx *context.APIContext) { + packageName, packageVersion, filename, err := sanitizeParameters(ctx) + if err != nil { + ctx.Error(http.StatusBadRequest, "", err) + return + } + + p, err := models.GetPackageByNameAndVersion(ctx.Repo.Repository.ID, models.PackageGeneric, packageName, packageVersion) + if err != nil { + if err == models.ErrPackageNotExist { + ctx.Error(http.StatusBadRequest, "", err) + return + } + log.Error("Error getting package: %v", err) + ctx.Error(http.StatusInternalServerError, "", "") + return + } + + pfs, err := p.GetFiles() + if err != nil { + log.Error("Error getting package files: %v", err) + ctx.Error(http.StatusInternalServerError, "", "") + return + } + + pf := pfs[0] + + if !strings.EqualFold(pf.LowerName, filename) { + ctx.Error(http.StatusBadRequest, "", models.ErrPackageNotExist) + return + } + + packageStore := packages.NewContentStore() + s, err := packageStore.Get(pf.ID) + if err != nil { + log.Error("Error reading package file: %v", err) + ctx.Error(http.StatusInternalServerError, "", "") + return + } + defer s.Close() + + ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, pf.Name)) + ctx.Resp.Header().Set("Content-Type", "application/octet-stream") + _, err = io.Copy(ctx.Resp, s) + if err != nil { + log.Error("Error in io.Copy: %v", err) + } +} + +// UploadPackage uploads the specific generic package. +// Duplicated packages get rejected. +func UploadPackage(ctx *context.APIContext) { + packageName, packageVersion, filename, err := sanitizeParameters(ctx) + if err != nil { + ctx.Error(http.StatusBadRequest, "", err) + return + } + + defer ctx.Req.Body.Close() + r, err := filebuffer.CreateFromReader(ctx.Req.Body, 32*1024*1024) + if err != nil { + log.Error("Error in CreateFromReader: %v", err) + ctx.Error(http.StatusInternalServerError, "", "") + return + } + defer r.Close() + + p, err := package_service.CreatePackage( + ctx.User, + ctx.Repo.Repository, + models.PackageGeneric, + packageName, + packageVersion, + nil, + false, + ) + if err != nil { + if err == models.ErrDuplicatePackage { + ctx.Error(http.StatusBadRequest, "", err) + return + } + log.Error("Error in CreatePackage: %v", err) + ctx.Error(http.StatusInternalServerError, "", "") + return + } + + _, err = package_service.AddFileToPackage(p, filename, r.Size(), r) + if err != nil { + log.Error("Error in AddFileToPackage: %v", err) + if err := models.DeletePackageByID(p.ID); err != nil { + log.Error("Error in DeletePackageByID: %v", err) + } + ctx.Error(http.StatusInternalServerError, "", "") + } +} + +// DeletePackage deletes the specific generic package. +func DeletePackage(ctx *context.APIContext) { + packageName, packageVersion, _, err := sanitizeParameters(ctx) + if err != nil { + ctx.Error(http.StatusBadRequest, "", err) + return + } + + err = package_service.DeletePackage(ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion) + if err != nil { + if err == models.ErrPackageNotExist { + ctx.Error(http.StatusBadRequest, "", err) + return + } + ctx.Error(http.StatusInternalServerError, "", "") + } +} + +func sanitizeParameters(ctx *context.APIContext) (packageName, packageVersion, filename string, err error) { + packageName = ctx.Params("packagename") + packageVersion = ctx.Params("packageversion") + filename = ctx.Params("filename") + + if !packageNameRegex.MatchString(packageName) || !packageVersionRegex.MatchString(packageVersion) || !filenameRegex.MatchString(filename) { + err = errors.New("Invalid package name, package version or filename") + } + return +} diff --git a/services/packages/packages.go b/services/packages/packages.go new file mode 100644 index 0000000000000..f9b4f18bbbf33 --- /dev/null +++ b/services/packages/packages.go @@ -0,0 +1,95 @@ +// Copyright 2021 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 packages + +import ( + "io" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + packages_module "code.gitea.io/gitea/modules/packages" +) + +// CreatePackage creates a new package +func CreatePackage(creator *models.User, repository *models.Repository, packageType models.PackageType, name, version string, metaData interface{}, allowDuplicate bool) (*models.Package, error) { + p := &models.Package{ + RepositoryID: repository.ID, + CreatorID: creator.ID, + Type: packageType, + Name: name, + LowerName: strings.ToLower(name), + Version: version, + MetaData: metaData, + } + if err := models.TryInsertPackage(p); err != nil { + if err == models.ErrDuplicatePackage { + if allowDuplicate { + return p, nil + } + return nil, err + } + log.Error("Error inserting package: %v", err) + return nil, err + } + return p, nil +} + +// AddFileToPackage adds a new file to package and stores its content +func AddFileToPackage(p *models.Package, filename string, size int64, r io.Reader) (*models.PackageFile, error) { + pf := &models.PackageFile{ + PackageID: p.ID, + Size: size, + Name: filename, + LowerName: strings.ToLower(filename), + } + if err := models.InsertPackageFile(pf); err != nil { + log.Error("Error inserting package file: %v", err) + return nil, err + } + + packageStore := packages_module.NewContentStore() + if err := packageStore.Save(pf.ID, r, size); err != nil { + log.Error("Error saving package file: %v", err) + if err := models.DeletePackageFileByID(pf.ID); err != nil { + log.Error("Error deleting package file: %v", err) + } + return nil, err + } + return pf, nil +} + +// DeletePackage deletes a package and all associated files +func DeletePackage(repository *models.Repository, packageType models.PackageType, name, version string) error { + p, err := models.GetPackageByNameAndVersion(repository.ID, packageType, name, version) + if err != nil { + if err == models.ErrPackageNotExist { + return err + } + log.Error("Error getting package: %v", err) + return err + } + + pfs, err := p.GetFiles() + if err != nil { + log.Error("Error getting package files: %v", err) + return err + } + + contentStore := packages_module.NewContentStore() + for _, pf := range pfs { + if err := contentStore.Delete(pf.ID); err != nil { + log.Error("Error deleting package file: %v", err) + return err + } + } + + if err := models.DeletePackageByID(p.ID); err != nil { + log.Error("Error deleting package: %v", err) + return err + } + + return nil +} From 3c86d2b7f8658dd384d1a2375455028b928b262e Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sun, 11 Jul 2021 20:24:22 +0000 Subject: [PATCH 004/130] Added tests. --- integrations/api_packages_generic_test.go | 79 ++++++++++++++++++++++ modules/context/context.go | 2 +- routers/api/v1/packages/generic/generic.go | 17 ++--- 3 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 integrations/api_packages_generic_test.go diff --git a/integrations/api_packages_generic_test.go b/integrations/api_packages_generic_test.go new file mode 100644 index 0000000000000..a73a14051005c --- /dev/null +++ b/integrations/api_packages_generic_test.go @@ -0,0 +1,79 @@ +// Copyright 2017 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 ( + "bytes" + "fmt" + "net/http" + "testing" + + "code.gitea.io/gitea/models" + + "github.com/stretchr/testify/assert" +) + +func TestPackageGeneric(t *testing.T) { + defer prepareTestEnv(t)() + repository := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 2}).(*models.Repository) + user := models.AssertExistsAndLoadBean(t, &models.User{ID: repository.OwnerID}).(*models.User) + session := loginUser(t, user.Name) + token := getTokenForLoggedInUser(t, session) + + packageName := "te-st_pac.kage" + packageVersion := "v1.0.3" + filename := "fi-le_na.me" + content := []byte{1,2,3} + + url := fmt.Sprintf("/api/v1/repos/%s/%s/packages/generic/%s/%s/%s?token=%s", user.Name, repository.Name, packageName, packageVersion, filename, token) + + t.Run("Upload", func(t *testing.T) { + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)) + MakeRequest(t, req, http.StatusCreated) + + ps, err := models.GetPackagesByRepositoryID(repository.ID) + assert.NoError(t, err) + assert.Len(t, ps, 1) + assert.Equal(t, packageName, ps[0].Name) + assert.Equal(t, packageVersion, ps[0].Version) + + pfs, err := ps[0].GetFiles() + assert.NoError(t, err) + assert.Len(t, pfs, 1) + assert.Equal(t, filename, pfs[0].Name) + assert.Equal(t, int64(len(content)), pfs[0].Size) + }) + + t.Run("UploadExists", func(t *testing.T) { + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)) + MakeRequest(t, req, http.StatusBadRequest) + }) + + t.Run("Download", func(t *testing.T) { + req := NewRequest(t, "GET", url) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, content, resp.Body.Bytes()) + }) + + t.Run("Delete", func(t *testing.T) { + req := NewRequest(t, "DELETE", url) + MakeRequest(t, req, http.StatusOK) + + ps, err := models.GetPackagesByRepositoryID(repository.ID) + assert.NoError(t, err) + assert.Empty(t, ps) + }) + + t.Run("DownloadNotExists", func(t *testing.T) { + req := NewRequest(t, "GET", url) + MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("DeleteNotExists", func(t *testing.T) { + req := NewRequest(t, "DELETE", url) + MakeRequest(t, req, http.StatusNotFound) + }) +} diff --git a/modules/context/context.go b/modules/context/context.go index 64f8b12084576..978459ed725c6 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -358,7 +358,7 @@ func (ctx *Context) PlainText(status int, bs []byte) { ctx.Resp.WriteHeader(status) ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8") if _, err := ctx.Resp.Write(bs); err != nil { - ctx.ServerError("Render JSON failed", err) + ctx.ServerError("Write failed", err) } } diff --git a/routers/api/v1/packages/generic/generic.go b/routers/api/v1/packages/generic/generic.go index c713fa7153064..57023144cc7a2 100644 --- a/routers/api/v1/packages/generic/generic.go +++ b/routers/api/v1/packages/generic/generic.go @@ -6,8 +6,6 @@ package generic import ( "errors" - "fmt" - "io" "net/http" "regexp" "strings" @@ -36,7 +34,7 @@ func DownloadPackage(ctx *context.APIContext) { p, err := models.GetPackageByNameAndVersion(ctx.Repo.Repository.ID, models.PackageGeneric, packageName, packageVersion) if err != nil { if err == models.ErrPackageNotExist { - ctx.Error(http.StatusBadRequest, "", err) + ctx.Error(http.StatusNotFound, "", err) return } log.Error("Error getting package: %v", err) @@ -54,7 +52,7 @@ func DownloadPackage(ctx *context.APIContext) { pf := pfs[0] if !strings.EqualFold(pf.LowerName, filename) { - ctx.Error(http.StatusBadRequest, "", models.ErrPackageNotExist) + ctx.Error(http.StatusNotFound, "", models.ErrPackageNotExist) return } @@ -67,12 +65,7 @@ func DownloadPackage(ctx *context.APIContext) { } defer s.Close() - ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, pf.Name)) - ctx.Resp.Header().Set("Content-Type", "application/octet-stream") - _, err = io.Copy(ctx.Resp, s) - if err != nil { - log.Error("Error in io.Copy: %v", err) - } + ctx.ServeStream(s, pf.Name) } // UploadPackage uploads the specific generic package. @@ -120,6 +113,8 @@ func UploadPackage(ctx *context.APIContext) { } ctx.Error(http.StatusInternalServerError, "", "") } + + ctx.PlainText(http.StatusCreated, nil) } // DeletePackage deletes the specific generic package. @@ -133,7 +128,7 @@ func DeletePackage(ctx *context.APIContext) { err = package_service.DeletePackage(ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion) if err != nil { if err == models.ErrPackageNotExist { - ctx.Error(http.StatusBadRequest, "", err) + ctx.Error(http.StatusNotFound, "", err) return } ctx.Error(http.StatusInternalServerError, "", "") From 8d922ff1777bcc69ecded48f7b8dccfc893e4a08 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 13 Jul 2021 16:29:37 +0000 Subject: [PATCH 005/130] Added NuGet package registry. --- integrations/api_packages_generic_test.go | 4 +- integrations/api_packages_nuget_test.go | 192 ++++++++++++++ models/migrations/v188.go | 16 +- models/package.go | 58 +++- modules/context/context.go | 20 ++ modules/packages/nuget/metadata.go | 151 +++++++++++ modules/packages/nuget/metadata_test.go | 112 ++++++++ modules/util/filebuffer/file_backed_buffer.go | 10 +- routers/api/v1/api.go | 24 +- routers/api/v1/packages/generic/generic.go | 77 +++--- routers/api/v1/packages/nuget/api.go | 251 ++++++++++++++++++ routers/api/v1/packages/nuget/links.go | 28 ++ routers/api/v1/packages/nuget/nuget.go | 248 +++++++++++++++++ routers/api/v1/packages/nuget/package.go | 65 +++++ services/packages/packages.go | 48 +++- 15 files changed, 1223 insertions(+), 81 deletions(-) create mode 100644 integrations/api_packages_nuget_test.go create mode 100644 modules/packages/nuget/metadata.go create mode 100644 modules/packages/nuget/metadata_test.go create mode 100644 routers/api/v1/packages/nuget/api.go create mode 100644 routers/api/v1/packages/nuget/links.go create mode 100644 routers/api/v1/packages/nuget/nuget.go create mode 100644 routers/api/v1/packages/nuget/package.go diff --git a/integrations/api_packages_generic_test.go b/integrations/api_packages_generic_test.go index a73a14051005c..89e099b3fd365 100644 --- a/integrations/api_packages_generic_test.go +++ b/integrations/api_packages_generic_test.go @@ -23,9 +23,9 @@ func TestPackageGeneric(t *testing.T) { token := getTokenForLoggedInUser(t, session) packageName := "te-st_pac.kage" - packageVersion := "v1.0.3" + packageVersion := "1.0.3" filename := "fi-le_na.me" - content := []byte{1,2,3} + content := []byte{1, 2, 3} url := fmt.Sprintf("/api/v1/repos/%s/%s/packages/generic/%s/%s/%s?token=%s", user.Name, repository.Name, packageName, packageVersion, filename, token) diff --git a/integrations/api_packages_nuget_test.go b/integrations/api_packages_nuget_test.go new file mode 100644 index 0000000000000..c5e08a96cc156 --- /dev/null +++ b/integrations/api_packages_nuget_test.go @@ -0,0 +1,192 @@ +// Copyright 2017 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/zip" + "bytes" + "fmt" + "net/http" + "testing" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/routers/api/v1/packages/nuget" + + "github.com/stretchr/testify/assert" +) + +func TestPackageNuGet(t *testing.T) { + defer prepareTestEnv(t)() + repository := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 2}).(*models.Repository) + user := models.AssertExistsAndLoadBean(t, &models.User{ID: repository.OwnerID}).(*models.User) + + packageName := "test.package" + packageVersion := "1.0.3" + packageAuthors := "KN4CK3R" + packageDescription := "Gitea Test Package" + + var buf bytes.Buffer + archive := zip.NewWriter(&buf) + w, _ := archive.Create("package.nuspec") + w.Write([]byte(` + + + `+packageName+` + `+packageVersion+` + `+packageAuthors+` + `+packageDescription+` + + `)) + archive.Close() + content := buf.Bytes() + + url := fmt.Sprintf("/api/v1/repos/%s/%s/packages/nuget", user.Name, repository.Name) + + t.Run("ServiceIndex", func(t *testing.T) { + req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url)) + req = AddBasicAuthHeader(req, user.Name) + _ = MakeRequest(t, req, http.StatusOK) + + + }) + + t.Run("Upload", func(t *testing.T) { + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusCreated) + + ps, err := models.GetPackagesByRepositoryID(repository.ID) + assert.NoError(t, err) + assert.Len(t, ps, 1) + assert.Equal(t, packageName, ps[0].Name) + assert.Equal(t, packageVersion, ps[0].Version) + + pfs, err := ps[0].GetFiles() + assert.NoError(t, err) + assert.Len(t, pfs, 1) + assert.Equal(t, fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion), pfs[0].Name) + assert.Equal(t, int64(len(content)), pfs[0].Size) + }) + + t.Run("UploadExists", func(t *testing.T) { + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusBadRequest) + }) + + t.Run("Download", func(t *testing.T) { + req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", url, packageName, packageVersion, packageName, packageVersion)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, content, resp.Body.Bytes()) + }) + + t.Run("SearchService", func(t *testing.T) { + cases := []struct{ + Query string + Skip int + Take int + ExpectedTotal int64 + ExpectedResults int + } { + {"", 0, 0, 1, 1}, + {"", 0, 10, 1, 1}, + {"gitea", 0, 10, 0, 0}, + {"test", 0, 10, 1, 1}, + {"test", 1, 10, 1, 0}, + } + + for i, c := range cases { + req := NewRequest(t, "GET", fmt.Sprintf("%s/query?q=%s&skip=%d&take=%d", url, c.Query, c.Skip, c.Take)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + var result nuget.SearchResultResponse + DecodeJSON(t, resp, &result) + + assert.Equal(t, c.ExpectedTotal, result.TotalHits, "case %d: unexpected total hits", i) + assert.Len(t, result.Data, c.ExpectedResults, "case %d: unexpected result count", i) + } + }) + + t.Run("RegistrationService", func(t *testing.T) { + indexURL := fmt.Sprintf("%s%s/registration/%s/index.json", setting.AppURL, url[1:], packageName) + leafURL := fmt.Sprintf("%s%s/registration/%s/%s.json", setting.AppURL, url[1:], packageName, packageVersion) + contentURL := fmt.Sprintf("%s%s/package/%s/%s/%s.%s.nupkg", setting.AppURL, url[1:], packageName, packageVersion, packageName, packageVersion) + + t.Run("RegistrationIndex", func(t *testing.T) { + req := NewRequest(t, "GET", fmt.Sprintf("%s/registration/%s/index.json", url, packageName)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + var result nuget.RegistrationIndexResponse + DecodeJSON(t, resp, &result) + + assert.Equal(t, indexURL, result.RegistrationIndexURL) + assert.Equal(t, 1, result.Count) + assert.Len(t, result.Pages, 1) + assert.Equal(t, indexURL, result.Pages[0].RegistrationPageURL) + assert.Equal(t, packageVersion, result.Pages[0].Lower) + assert.Equal(t, packageVersion, result.Pages[0].Upper) + assert.Equal(t, 1, result.Pages[0].Count) + assert.Len(t, result.Pages[0].Items, 1) + assert.Equal(t, packageName, result.Pages[0].Items[0].CatalogEntry.ID) + assert.Equal(t, packageVersion, result.Pages[0].Items[0].CatalogEntry.Version) + assert.Equal(t, packageAuthors, result.Pages[0].Items[0].CatalogEntry.Authors) + assert.Equal(t, packageDescription, result.Pages[0].Items[0].CatalogEntry.Description) + assert.Equal(t, leafURL, result.Pages[0].Items[0].CatalogEntry.CatalogLeafURL) + assert.Equal(t, contentURL, result.Pages[0].Items[0].CatalogEntry.PackageContentURL) + }) + + t.Run("RegistrationLeaf", func(t *testing.T) { + req := NewRequest(t, "GET", fmt.Sprintf("%s/registration/%s/%s.json", url, packageName, packageVersion)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + var result nuget.RegistrationLeafResponse + DecodeJSON(t, resp, &result) + + assert.Equal(t, leafURL, result.RegistrationLeafURL) + assert.Equal(t, contentURL, result.PackageContentURL) + assert.Equal(t, indexURL, result.RegistrationIndexURL) + }) + }) + + t.Run("PackageService", func(t *testing.T) { + req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/index.json", url, packageName)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + var result nuget.PackageVersionsResponse + DecodeJSON(t, resp, &result) + + assert.Len(t, result.Versions, 1) + assert.Equal(t, packageVersion, result.Versions[0]) + }) + + t.Run("Delete", func(t *testing.T) { + req := NewRequest(t, "DELETE", fmt.Sprintf("%s/package/%s/%s", url, packageName, packageVersion)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusOK) + + ps, err := models.GetPackagesByRepositoryID(repository.ID) + assert.NoError(t, err) + assert.Empty(t, ps) + }) + + t.Run("DownloadNotExists", func(t *testing.T) { + req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", url, packageName, packageVersion, packageName, packageVersion)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("DeleteNotExists", func(t *testing.T) { + req := NewRequest(t, "DELETE", fmt.Sprintf("%s/package/%s/%s", url, packageName, packageVersion)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusNotFound) + }) +} diff --git a/models/migrations/v188.go b/models/migrations/v188.go index 4f41b54f9f35b..5f8fb3aea52e5 100644 --- a/models/migrations/v188.go +++ b/models/migrations/v188.go @@ -12,14 +12,14 @@ import ( func addPackageTables(x *xorm.Engine) error { type Package struct { - ID int64 `xorm:"pk autoincr"` - RepositoryID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` - CreatorID int64 - Type int `xorm:"UNIQUE(s) INDEX NOT NULL"` - Name string - LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` - Version string `xorm:"UNIQUE(s) INDEX NOT NULL"` - MetaData interface{} `xorm:"TEXT JSON"` + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + CreatorID int64 + Type int `xorm:"UNIQUE(s) INDEX NOT NULL"` + Name string + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + Version string `xorm:"UNIQUE(s) INDEX NOT NULL"` + MetadataRaw string `xorm:"TEXT"` CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated"` diff --git a/models/package.go b/models/package.go index 351a78fd25979..0ee54f91ef661 100644 --- a/models/package.go +++ b/models/package.go @@ -9,6 +9,9 @@ import ( "strings" "code.gitea.io/gitea/modules/timeutil" + + "github.com/hashicorp/go-version" + "xorm.io/builder" ) // PackageType specifies the different package types @@ -17,6 +20,7 @@ type PackageType int // Note: new type must append to the end of list to maintain compatibility. const ( PackageGeneric PackageType = iota + PackageNuGet // 1 ) var ( @@ -28,14 +32,15 @@ var ( // Package represents a package type Package struct { - ID int64 `xorm:"pk autoincr"` - RepositoryID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` - CreatorID int64 - Type PackageType `xorm:"UNIQUE(s) INDEX NOT NULL"` - Name string - LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` - Version string `xorm:"UNIQUE(s) INDEX NOT NULL"` - MetaData interface{} `xorm:"TEXT JSON"` + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + CreatorID int64 + Type PackageType `xorm:"UNIQUE(s) INDEX NOT NULL"` + Name string + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + Version string `xorm:"UNIQUE(s) INDEX NOT NULL"` + SemVer *version.Version `xorm:"-"` + MetadataRaw string `xorm:"TEXT"` CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated"` @@ -113,22 +118,22 @@ func DeletePackagesByRepositoryID(repositoryID int64) error { // GetPackagesByRepositoryID returns all packages of a repository func GetPackagesByRepositoryID(repositoryID int64) ([]*Package, error) { packages := make([]*Package, 0, 10) - return packages, x.Where("repository_id = ?", repositoryID).Find(&packages) + return packages, x.Where("repo_id = ?", repositoryID).Find(&packages) } // GetPackagesByName gets all repository packages with the specific name func GetPackagesByName(repositoryID int64, packageType PackageType, packageName string) ([]*Package, error) { packages := make([]*Package, 0, 10) - return packages, x.Where("repository_id = ? AND type = ? AND lower_name = ?", repositoryID, packageType, strings.ToLower(packageName)).Find(&packages) + return packages, x.Where("repo_id = ? AND type = ? AND lower_name = ?", repositoryID, packageType, strings.ToLower(packageName)).Find(&packages) } // GetPackageByNameAndVersion gets a repository package by name and version func GetPackageByNameAndVersion(repositoryID int64, packageType PackageType, packageName, packageVersion string) (*Package, error) { p := &Package{ - RepositoryID: repositoryID, - Type: packageType, - LowerName: strings.ToLower(packageName), - Version: packageVersion, + RepoID: repositoryID, + Type: packageType, + LowerName: strings.ToLower(packageName), + Version: packageVersion, } has, err := x.Get(p) if err != nil { @@ -139,6 +144,31 @@ func GetPackageByNameAndVersion(repositoryID int64, packageType PackageType, pac return p, nil } +// SearchPackages searches for packages by name and can be used to navigate through the package list +func SearchPackages(repositoryID int64, packageType PackageType, query string, skip, take int) (int64, []*Package, error) { + cond := builder.NewCond() + cond = cond.And(builder.Eq{"repo_id": repositoryID}) + cond = cond.And(builder.Eq{"type": packageType}) + if query != "" { + cond = cond.And(builder.Like{"lower_name", strings.ToLower(query)}) + } + + if take <= 0 || take > 100 { + take = 100 + } + + sess := x.Where(cond) + if skip > 0 { + sess = sess.Limit(take, skip) + } else { + sess = sess.Limit(take) + } + + packages := make([]*Package, 0, take) + count, err := sess.FindAndCount(&packages) + return count, packages, err +} + // InsertPackageFile inserts a package file func InsertPackageFile(pf *PackageFile) error { _, err := x.Insert(pf) diff --git a/modules/context/context.go b/modules/context/context.go index 978459ed725c6..e575e90e10158 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -325,6 +325,26 @@ func (ctx *Context) QueryOptionalBool(key string, defaults ...util.OptionalBool) return (*Forms)(ctx.Req).MustOptionalBool(key, defaults...) } +// UploadStream returns the request body or the first form file +func (ctx *Context) UploadStream() (io.ReadCloser, error) { + contentType := strings.ToLower(ctx.Req.Header.Get("Content-Type")) + if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || strings.HasPrefix(contentType, "multipart/form-data") { + if err := ctx.Req.ParseMultipartForm(32 * 1024 * 1024); err != nil { + return nil, err + } + if ctx.Req.MultipartForm.File == nil { + return nil, http.ErrMissingFile + } + for _, files := range ctx.Req.MultipartForm.File { + if len(files) > 0 { + return files[0].Open() + } + } + return nil, http.ErrMissingFile + } + return ctx.Req.Body, nil +} + // HandleText handles HTTP status code func (ctx *Context) HandleText(status int, title string) { if (status/100 == 4) || (status/100 == 5) { diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go new file mode 100644 index 0000000000000..6ca2de0d9cc95 --- /dev/null +++ b/modules/packages/nuget/metadata.go @@ -0,0 +1,151 @@ +// Copyright 2021 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 nuget + +import ( + "archive/zip" + "encoding/xml" + "errors" + "io" + "path/filepath" + "regexp" + "strings" + + "github.com/hashicorp/go-version" +) + +var ( + // ErrMissingNuspecFile indicates a missing Nuspec file + ErrMissingNuspecFile = errors.New("Nuspec file is missing") + // ErrNuspecFileTooLarge indicates a Nuspec file which is too large + ErrNuspecFileTooLarge = errors.New("Nuspec file is too large") + // ErrNuspecInvalidID indicates an invalid id in the Nuspec file + ErrNuspecInvalidID = errors.New("Nuspec file contains an invalid id") + // ErrNuspecInvalidVersion indicates an invalid version in the Nuspec file + ErrNuspecInvalidVersion = errors.New("Nuspec file contains an invalid version") +) + +var idmatch = regexp.MustCompile(`\A\w+(?:[.-]\w+)*\z`) + +const maxNuspecFileSize = 3 * 1024 * 1024 + +// Metadata represents the metadata of a Nuget package +type Metadata struct { + ID string `json:"-"` + Version string `json:"-"` + Description string + Summary string + ReleaseNotes string + Authors string + RequireLicenseAcceptance bool + ProjectURL string `json:"project_url"` + RepositoryURL string `json:"repository_url"` + Dependencies map[string][]Dependency +} + +// Dependency represents a dependency of a Nuget package +type Dependency struct { + ID string + Version string +} + +type nuspecPackage struct { + Metadata struct { + ID string `xml:"id"` + Version string `xml:"version"` + Authors string `xml:"authors"` + RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"` + ProjectURL string `xml:"projectUrl"` + Description string `xml:"description"` + Summary string `xml:"summary"` + ReleaseNotes string `xml:"releaseNotes"` + Repository struct { + URL string `xml:"url,attr"` + } `xml:"repository"` + Dependencies struct { + Group []struct { + TargetFramework string `xml:"targetFramework,attr"` + Dependency []struct { + ID string `xml:"id,attr"` + Version string `xml:"version,attr"` + Exclude string `xml:"exclude,attr"` + } `xml:"dependency"` + } `xml:"group"` + } `xml:"dependencies"` + } `xml:"metadata"` +} + +// ParsePackageMetaData parses the metadata of a Nuget package file +func ParsePackageMetaData(r io.ReaderAt, size int64) (*Metadata, error) { + archive, err := zip.NewReader(r, size) + if err != nil { + return nil, err + } + + for _, file := range archive.File { + if filepath.Dir(file.Name) != "." { + continue + } + if strings.HasSuffix(strings.ToLower(file.Name), ".nuspec") { + if file.UncompressedSize64 > maxNuspecFileSize { + return nil, ErrNuspecFileTooLarge + } + f, err := archive.Open(file.Name) + if err != nil { + return nil, err + } + defer f.Close() + + return ParseNuspecMetaData(f) + } + } + return nil, ErrMissingNuspecFile +} + +// ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package +func ParseNuspecMetaData(r io.Reader) (*Metadata, error) { + var p nuspecPackage + dec := xml.NewDecoder(r) + err := dec.Decode(&p) + if err != nil { + return nil, err + } + + if !idmatch.MatchString(p.Metadata.ID) { + return nil, ErrNuspecInvalidID + } + + v, err := version.NewSemver(p.Metadata.Version) + if err != nil { + return nil, ErrNuspecInvalidVersion + } + + m := &Metadata{ + ID: p.Metadata.ID, + Version: v.String(), + Description: p.Metadata.Description, + Summary: p.Metadata.Summary, + ReleaseNotes: p.Metadata.ReleaseNotes, + Authors: p.Metadata.Authors, + RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance, + ProjectURL: p.Metadata.ProjectURL, + RepositoryURL: p.Metadata.Repository.URL, + Dependencies: make(map[string][]Dependency), + } + for _, group := range p.Metadata.Dependencies.Group { + deps := make([]Dependency, 0, len(group.Dependency)) + for _, dep := range group.Dependency { + if dep.ID == "" || dep.Version == "" { + continue + } + deps = append(deps, Dependency{ + ID: dep.ID, + Version: dep.Version, + }) + } + m.Dependencies[group.TargetFramework] = deps + } + return m, nil +} diff --git a/modules/packages/nuget/metadata_test.go b/modules/packages/nuget/metadata_test.go new file mode 100644 index 0000000000000..87c73c53ebfe2 --- /dev/null +++ b/modules/packages/nuget/metadata_test.go @@ -0,0 +1,112 @@ +// Copyright 2021 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 nuget + +import ( + "archive/zip" + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + id = "System.Gitea" + semver = "1.0.1" + authors = "Gitea Authors" + projectURL = "https://gitea.com" + description = "Package Description" + summary = "Package Summary" + releaseNotes = "Package Release Notes" + repositoryURL = "https://gitea.com/gitea/gitea" + targetFramework = ".NETStandard2.1" + dependencyID = "System.Text.Json" + dependencyVersion = "5.0.0" +) + +const nuspecContent = ` + + + ` + id + ` + ` + semver + ` + ` + authors + ` + true + ` + projectURL + ` + ` + description + ` + ` + summary + ` + ` + releaseNotes + ` + + + + + + + +` + +func TestParsePackageMetaData(t *testing.T) { + createArchive := func(name, content string) []byte { + var buf bytes.Buffer + archive := zip.NewWriter(&buf) + w, _ := archive.Create(name) + w.Write([]byte(content)) + archive.Close() + return buf.Bytes() + } + + t.Run("MissingNuspecFile", func(t *testing.T) { + data := createArchive("dummy.txt", "") + + m, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data))) + assert.Nil(t, m) + assert.ErrorIs(t, err, ErrMissingNuspecFile) + }) + + t.Run("MissingNuspecFileInRoot", func(t *testing.T) { + data := createArchive("sub/package.nuspec", "") + + m, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data))) + assert.Nil(t, m) + assert.ErrorIs(t, err, ErrMissingNuspecFile) + }) + + t.Run("InvalidNuspecFile", func(t *testing.T) { + data := createArchive("package.nuspec", "") + + m, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data))) + assert.Nil(t, m) + assert.ErrorIs(t, err, ErrNuspecInvalidID) + }) + + t.Run("Valid", func(t *testing.T) { + data := createArchive("package.nuspec", nuspecContent) + + m, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data))) + assert.NoError(t, err) + assert.NotNil(t, m) + }) +} + +func TestParseNuspecMetaData(t *testing.T) { + m, err := ParseNuspecMetaData(strings.NewReader(nuspecContent)) + assert.NoError(t, err) + assert.NotNil(t, m) + + assert.Equal(t, id, m.ID) + assert.Equal(t, semver, m.Version) + assert.Equal(t, authors, m.Authors) + assert.Equal(t, projectURL, m.ProjectURL) + assert.Equal(t, description, m.Description) + assert.Equal(t, summary, m.Summary) + assert.Equal(t, releaseNotes, m.ReleaseNotes) + assert.Equal(t, repositoryURL, m.RepositoryURL) + assert.Len(t, m.Dependencies, 1) + assert.Contains(t, m.Dependencies, targetFramework) + deps := m.Dependencies[targetFramework] + assert.Len(t, deps, 1) + assert.Equal(t, dependencyID, deps[0].ID) + assert.Equal(t, dependencyVersion, deps[0].Version) +} diff --git a/modules/util/filebuffer/file_backed_buffer.go b/modules/util/filebuffer/file_backed_buffer.go index fca4aa3f0f8aa..136f7d661e008 100644 --- a/modules/util/filebuffer/file_backed_buffer.go +++ b/modules/util/filebuffer/file_backed_buffer.go @@ -18,7 +18,7 @@ var ( ErrInvalidMemorySize = errors.New("Memory size must be greater 0 and lower math.MaxInt32") ) -// FileBackedBuffer implements io.ReadSeekCloser +// FileBackedBuffer implements io.ReadSeekCloser and io.ReaderAt type FileBackedBuffer struct { size int64 buffer *bytes.Reader @@ -69,6 +69,14 @@ func (b *FileBackedBuffer) Read(p []byte) (int, error) { return b.buffer.Read(p) } +// ReadAt implements io.ReaderAt +func (b *FileBackedBuffer) ReadAt(p []byte, off int64) (int, error) { + if b.tmpFile != nil { + return b.tmpFile.ReadAt(p, off) + } + return b.buffer.ReadAt(p, off) +} + // Seek implements io.Seeker func (b *FileBackedBuffer) Seek(offset int64, whence int) (int64, error) { if b.tmpFile != nil { diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index e860443c05f3f..ae64cb3b3b8ad 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -80,6 +80,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/notify" "code.gitea.io/gitea/routers/api/v1/org" "code.gitea.io/gitea/routers/api/v1/packages/generic" + "code.gitea.io/gitea/routers/api/v1/packages/nuget" "code.gitea.io/gitea/routers/api/v1/repo" "code.gitea.io/gitea/routers/api/v1/settings" _ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation @@ -217,6 +218,7 @@ func reqExploreSignIn() func(ctx *context.APIContext) { func reqBasicAuth() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.Context.IsBasicAuth { + ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="API"`) ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "basic auth required") return } @@ -974,12 +976,28 @@ func Routes() *web.Route { m.Group("/packages", func() { m.Group("/generic", func() { m.Group("/{packagename}/{packageversion}/{filename}", func() { - m.Get("", generic.DownloadPackage) + m.Get("", generic.DownloadPackageContent) m.Put("" /*reqRepoWriter(models.UnitTypePackage),*/, generic.UploadPackage) m.Delete("" /*reqRepoWriter(models.UnitTypePackage),*/, generic.DeletePackage) }) - }) - }, reqToken()) + }, reqToken()) + m.Group("/nuget", func() { + m.Put("/" /*reqRepoWriter(models.UnitTypePackage),*/, nuget.UploadPackage) + m.Get("/index.json", nuget.ServiceIndex) + m.Get("/query", nuget.SearchService) + m.Group("/registration/{id}", func() { + m.Get("/index.json", nuget.RegistrationIndex) + m.Get("/{version}", nuget.RegistrationLeaf) + }) + m.Group("/package/{id}", func() { + m.Get("/index.json", nuget.EnumeratePackageVersions) + m.Group("/{version}", func() { + m.Delete("/" /*reqRepoWriter(models.UnitTypePackage),*/, nuget.DeletePackage) + m.Get("/{filename}", nuget.DownloadPackageContent) + }) + }) + }, reqBasicAuth()) + }) }, repoAssignment()) }) diff --git a/routers/api/v1/packages/generic/generic.go b/routers/api/v1/packages/generic/generic.go index 57023144cc7a2..5b65264ed1e3d 100644 --- a/routers/api/v1/packages/generic/generic.go +++ b/routers/api/v1/packages/generic/generic.go @@ -8,59 +8,35 @@ import ( "errors" "net/http" "regexp" - "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/packages" "code.gitea.io/gitea/modules/util/filebuffer" package_service "code.gitea.io/gitea/services/packages" + + "github.com/hashicorp/go-version" ) var packageNameRegex = regexp.MustCompile(`\A[A-Za-z0-9\.\_\-\+]+\z`) -var packageVersionRegex = regexp.MustCompile(`\A(?:\.?[\w\+-]+\.?)+\z`) var filenameRegex = packageNameRegex -// DownloadPackage serves the specific generic package. -func DownloadPackage(ctx *context.APIContext) { +// DownloadPackageContent serves the specific generic package. +func DownloadPackageContent(ctx *context.APIContext) { packageName, packageVersion, filename, err := sanitizeParameters(ctx) if err != nil { ctx.Error(http.StatusBadRequest, "", err) return } - p, err := models.GetPackageByNameAndVersion(ctx.Repo.Repository.ID, models.PackageGeneric, packageName, packageVersion) + s, pf, err := package_service.GetPackageFileStream(ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion, filename) if err != nil { if err == models.ErrPackageNotExist { ctx.Error(http.StatusNotFound, "", err) return } - log.Error("Error getting package: %v", err) - ctx.Error(http.StatusInternalServerError, "", "") - return - } - - pfs, err := p.GetFiles() - if err != nil { - log.Error("Error getting package files: %v", err) - ctx.Error(http.StatusInternalServerError, "", "") - return - } - - pf := pfs[0] - - if !strings.EqualFold(pf.LowerName, filename) { - ctx.Error(http.StatusNotFound, "", models.ErrPackageNotExist) - return - } - - packageStore := packages.NewContentStore() - s, err := packageStore.Get(pf.ID) - if err != nil { - log.Error("Error reading package file: %v", err) - ctx.Error(http.StatusInternalServerError, "", "") + ctx.Error(http.StatusInternalServerError, "", err) return } defer s.Close() @@ -77,14 +53,20 @@ func UploadPackage(ctx *context.APIContext) { return } - defer ctx.Req.Body.Close() - r, err := filebuffer.CreateFromReader(ctx.Req.Body, 32*1024*1024) + upload, err := ctx.UploadStream() + if err != nil { + ctx.Error(http.StatusBadRequest, "", err) + return + } + defer upload.Close() + + buf, err := filebuffer.CreateFromReader(upload, 32*1024*1024) if err != nil { - log.Error("Error in CreateFromReader: %v", err) + log.Error("Error creating file buffer: %v", err) ctx.Error(http.StatusInternalServerError, "", "") return } - defer r.Close() + defer buf.Close() p, err := package_service.CreatePackage( ctx.User, @@ -100,16 +82,16 @@ func UploadPackage(ctx *context.APIContext) { ctx.Error(http.StatusBadRequest, "", err) return } - log.Error("Error in CreatePackage: %v", err) + log.Error("Error creating package: %v", err) ctx.Error(http.StatusInternalServerError, "", "") return } - _, err = package_service.AddFileToPackage(p, filename, r.Size(), r) + _, err = package_service.AddFileToPackage(p, filename, buf.Size(), buf) if err != nil { - log.Error("Error in AddFileToPackage: %v", err) + log.Error("Error adding file to package: %v", err) if err := models.DeletePackageByID(p.ID); err != nil { - log.Error("Error in DeletePackageByID: %v", err) + log.Error("Error deleting package by id: %v", err) } ctx.Error(http.StatusInternalServerError, "", "") } @@ -135,13 +117,18 @@ func DeletePackage(ctx *context.APIContext) { } } -func sanitizeParameters(ctx *context.APIContext) (packageName, packageVersion, filename string, err error) { - packageName = ctx.Params("packagename") - packageVersion = ctx.Params("packageversion") - filename = ctx.Params("filename") +func sanitizeParameters(ctx *context.APIContext) (string, string, string, error) { + packageName := ctx.Params("packagename") + filename := ctx.Params("filename") + + if !packageNameRegex.MatchString(packageName) || !filenameRegex.MatchString(filename) { + return "", "", "", errors.New("Invalid package name or filename") + } - if !packageNameRegex.MatchString(packageName) || !packageVersionRegex.MatchString(packageVersion) || !filenameRegex.MatchString(filename) { - err = errors.New("Invalid package name, package version or filename") + v, err := version.NewSemver(ctx.Params("packageversion")) + if err != nil { + return "", "", "", err } - return + + return packageName, v.String(), filename, nil } diff --git a/routers/api/v1/packages/nuget/api.go b/routers/api/v1/packages/nuget/api.go new file mode 100644 index 0000000000000..f7586ab65b83c --- /dev/null +++ b/routers/api/v1/packages/nuget/api.go @@ -0,0 +1,251 @@ +// Copyright 2021 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 nuget + +import ( + "bytes" + "fmt" + "time" + + "github.com/hashicorp/go-version" +) + +// RegistrationIndexResponse https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#response +type RegistrationIndexResponse struct { + RegistrationIndexURL string `json:"@id"` + Type []string `json:"@type"` + Count int `json:"count"` + Pages []*RegistrationIndexPage `json:"items"` +} + +// RegistrationIndexPage https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-page-object +type RegistrationIndexPage struct { + RegistrationPageURL string `json:"@id"` + Lower string `json:"lower"` + Upper string `json:"upper"` + Count int `json:"count"` + Items []*RegistrationIndexPageItem `json:"items"` +} + +// RegistrationIndexPageItem https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf-object-in-a-page +type RegistrationIndexPageItem struct { + RegistrationLeafURL string `json:"@id"` + PackageContentURL string `json:"packageContent"` + CatalogEntry *CatalogEntry `json:"catalogEntry"` +} + +// CatalogEntry https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#catalog-entry +type CatalogEntry struct { + CatalogLeafURL string `json:"@id"` + PackageContentURL string `json:"packageContent"` + ID string `json:"id"` + Version string `json:"version"` + Description string `json:"description"` + Summary string `json:"summary"` + ReleaseNotes string `json:"releaseNotes"` + Authors string `json:"authors"` + RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"` + ProjectURL string `json:"projectURL"` + DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"` +} + +// PackageDependencyGroup https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency-group +type PackageDependencyGroup struct { + TargetFramework string `json:"targetFramework"` + Dependencies []*PackageDependency `json:"dependencies"` +} + +// PackageDependency https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency +type PackageDependency struct { + ID string `json:"id"` + Range string `json:"range"` +} + +func createRegistrationIndexResponse(l *linkBuilder, packages []*Package) *RegistrationIndexResponse { + sortedPackages := sortPackagesByVersionASC(packages) + + items := make([]*RegistrationIndexPageItem, 0, len(packages)) + for _, p := range sortedPackages { + items = append(items, createRegistrationIndexPageItem(l, p)) + } + + return &RegistrationIndexResponse{ + RegistrationIndexURL: l.GetRegistrationIndexURL(packages[0].Name), + Type: []string{"catalog:CatalogRoot", "PackageRegistration", "catalog:Permalink"}, + Count: 1, + Pages: []*RegistrationIndexPage{ + { + RegistrationPageURL: l.GetRegistrationIndexURL(packages[0].Name), + Count: len(packages), + Lower: normalizeVersion(packages[0].SemVer), + Upper: normalizeVersion(packages[len(packages)-1].SemVer), + Items: items, + }, + }, + } +} + +func createRegistrationIndexPageItem(l *linkBuilder, p *Package) *RegistrationIndexPageItem { + return &RegistrationIndexPageItem{ + RegistrationLeafURL: l.GetRegistrationLeafURL(p.Name, p.Version), + PackageContentURL: l.GetPackageDownloadURL(p.Name, p.Version), + CatalogEntry: &CatalogEntry{ + CatalogLeafURL: l.GetRegistrationLeafURL(p.Name, p.Version), + PackageContentURL: l.GetPackageDownloadURL(p.Name, p.Version), + ID: p.Name, + Version: p.Version, + Description: p.Metadata.Description, + Summary: p.Metadata.Summary, + ReleaseNotes: p.Metadata.ReleaseNotes, + Authors: p.Metadata.Authors, + RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance, + ProjectURL: p.Metadata.ProjectURL, + DependencyGroups: createDependencyGroups(p), + }, + } +} + +func createDependencyGroups(p *Package) []*PackageDependencyGroup { + dependencyGroups := make([]*PackageDependencyGroup, 0, len(p.Metadata.Dependencies)) + for k, v := range p.Metadata.Dependencies { + dependencies := make([]*PackageDependency, 0, len(v)) + for _, dep := range v { + dependencies = append(dependencies, &PackageDependency{ + ID: dep.ID, + Range: dep.Version, + }) + } + + dependencyGroups = append(dependencyGroups, &PackageDependencyGroup{ + TargetFramework: k, + Dependencies: dependencies, + }) + } + return dependencyGroups +} + +// RegistrationLeafResponse https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf +type RegistrationLeafResponse struct { + RegistrationLeafURL string `json:"@id"` + Type []string `json:"@type"` + Listed bool `json:"listed"` + PackageContentURL string `json:"packageContent"` + Published time.Time `json:"published"` + RegistrationIndexURL string `json:"registration"` +} + +func createRegistrationLeafResponse(l *linkBuilder, p *Package) *RegistrationLeafResponse { + return &RegistrationLeafResponse{ + Type: []string{"Package", "http://schema.nuget.org/catalog#Permalink"}, + Listed: true, + Published: time.Unix(int64(p.CreatedUnix), 0), + RegistrationLeafURL: l.GetRegistrationLeafURL(p.Name, p.Version), + PackageContentURL: l.GetPackageDownloadURL(p.Name, p.Version), + RegistrationIndexURL: l.GetRegistrationIndexURL(p.Name), + } +} + +// PackageVersionsResponse https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#response +type PackageVersionsResponse struct { + Versions []string `json:"versions"` +} + +func createPackageVersionsResponse(packages []*Package) *PackageVersionsResponse { + versions := make([]string, 0, len(packages)) + for _, p := range packages { + versions = append(versions, normalizeVersion(p.SemVer)) + } + + return &PackageVersionsResponse{ + Versions: versions, + } +} + +// SearchResultResponse https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#response +type SearchResultResponse struct { + TotalHits int64 `json:"totalHits"` + Data []*SearchResult `json:"data"` +} + +// SearchResult https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result +type SearchResult struct { + ID string `json:"id"` + Version string `json:"version"` + Versions []*SearchResultVersion `json:"versions"` + Description string `json:"description"` + Summary string `json:"summary"` + Authors string `json:"authors"` + ProjectURL string `json:"projectURL"` + RegistrationIndexURL string `json:"registration"` +} + +// SearchResultVersion https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result +type SearchResultVersion struct { + RegistrationLeafURL string `json:"@id"` + Version string `json:"version"` + Downloads int64 `json:"downloads"` +} + +func createSearchResultResponse(l *linkBuilder, totalHits int64, packages []*Package) *SearchResultResponse { + data := make([]*SearchResult, 0, len(packages)) + + if len(packages) > 0 { + groupID := packages[0].Name + group := make([]*Package, 0, 10) + + for i := 0; i < len(packages); i++ { + if groupID != packages[i].Name { + data = append(data, createSearchResult(l, group)) + groupID = packages[i].Name + group = group[:0] + } + group = append(group, packages[i]) + } + data = append(data, createSearchResult(l, group)) + } + + return &SearchResultResponse{ + TotalHits: totalHits, + Data: data, + } +} + +func createSearchResult(l *linkBuilder, packages []*Package) *SearchResult { + latest := packages[0] + versions := make([]*SearchResultVersion, 0, len(packages)) + for _, p := range packages { + if latest.SemVer.LessThan(p.SemVer) { + latest = p + } + + versions = append(versions, &SearchResultVersion{ + RegistrationLeafURL: l.GetRegistrationLeafURL(p.Name, p.Version), + Version: p.Version, + }) + } + + return &SearchResult{ + ID: latest.Name, + Version: latest.Version, + Versions: versions, + Description: latest.Metadata.Description, + Summary: latest.Metadata.Summary, + Authors: latest.Metadata.Authors, + ProjectURL: latest.Metadata.ProjectURL, + RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Name), + } +} + +// normalizeVersion removes the metadata +func normalizeVersion(v *version.Version) string { + var buf bytes.Buffer + segments := v.Segments64() + fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2]) + pre := v.Prerelease() + if pre != "" { + fmt.Fprintf(&buf, "-%s", pre) + } + return buf.String() +} diff --git a/routers/api/v1/packages/nuget/links.go b/routers/api/v1/packages/nuget/links.go new file mode 100644 index 0000000000000..82fa8ec1f94df --- /dev/null +++ b/routers/api/v1/packages/nuget/links.go @@ -0,0 +1,28 @@ +// Copyright 2021 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 nuget + +import ( + "fmt" +) + +type linkBuilder struct { + Base string +} + +// GetRegistrationIndexURL builds the registration index url +func (l *linkBuilder) GetRegistrationIndexURL(id string) string { + return fmt.Sprintf("%s/registration/%s/index.json", l.Base, id) +} + +// GetRegistrationIndexURL builds the registration leaf url +func (l *linkBuilder) GetRegistrationLeafURL(id, version string) string { + return fmt.Sprintf("%s/registration/%s/%s.json", l.Base, id, version) +} + +// GetRegistrationIndexURL builds the download url +func (l *linkBuilder) GetPackageDownloadURL(id, version string) string { + return fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", l.Base, id, version, id, version) +} diff --git a/routers/api/v1/packages/nuget/nuget.go b/routers/api/v1/packages/nuget/nuget.go new file mode 100644 index 0000000000000..6cda36e77c975 --- /dev/null +++ b/routers/api/v1/packages/nuget/nuget.go @@ -0,0 +1,248 @@ +// Copyright 2021 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 nuget + +import ( + //"errors" + "fmt" + "net/http" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + nuget_module "code.gitea.io/gitea/modules/packages/nuget" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util/filebuffer" + + package_service "code.gitea.io/gitea/services/packages" +) + +// ServiceIndex https://docs.microsoft.com/en-us/nuget/api/service-index +func ServiceIndex(ctx *context.APIContext) { + repoName := ctx.Repo.Repository.FullName() + + type resource struct { + ID string `json:"@id"` + Type string `json:"@type"` + } + + serviceIndex := struct { + Version string `json:"version"` + Resources []resource `json:"resources"` + }{ + Version: "3.0.0", + Resources: []resource{ + {ID: setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget/query", Type: "SearchQueryService"}, + {ID: setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget/query", Type: "SearchQueryService/3.0.0-beta"}, + {ID: setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget/query", Type: "SearchQueryService/3.0.0-rc"}, + {ID: setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget/registration", Type: "RegistrationsBaseUrl"}, + {ID: setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget/registration", Type: "RegistrationsBaseUrl/3.0.0-beta"}, + {ID: setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget/registration", Type: "RegistrationsBaseUrl/3.0.0-rc"}, + {ID: setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget/package", Type: "PackageBaseAddress/3.0.0"}, + {ID: setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget", Type: "PackagePublish/2.0.0"}, + }, + } + + ctx.JSON(http.StatusOK, serviceIndex) +} + +// SearchService https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages +func SearchService(ctx *context.APIContext) { + query := ctx.QueryTrim("q") + skip := ctx.QueryInt("skip") + take := ctx.QueryInt("take") + + total, packages, err := models.SearchPackages(ctx.Repo.Repository.ID, models.PackageNuGet, query, skip, take) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + nugetPackages, err := intializePackages(packages) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + resp := createSearchResultResponse( + &linkBuilder{setting.AppURL + "api/v1/repos/" + ctx.Repo.Repository.FullName() + "/packages/nuget"}, + total, + nugetPackages, + ) + + ctx.JSON(http.StatusOK, resp) +} + +// RegistrationIndex https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-index +func RegistrationIndex(ctx *context.APIContext) { + packageName := ctx.Params("id") + + packages, err := models.GetPackagesByName(ctx.Repo.Repository.ID, models.PackageNuGet, packageName) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + if len(packages) == 0 { + ctx.Error(http.StatusNotFound, "", err) + return + } + + nugetPackages, err := intializePackages(packages) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + resp := createRegistrationIndexResponse( + &linkBuilder{setting.AppURL + "api/v1/repos/" + ctx.Repo.Repository.FullName() + "/packages/nuget"}, + nugetPackages, + ) + + ctx.JSON(http.StatusOK, resp) +} + +// RegistrationLeaf https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf +func RegistrationLeaf(ctx *context.APIContext) { + packageName := ctx.Params("id") + packageVersion := strings.TrimSuffix(ctx.Params("version"), ".json") + + p, err := models.GetPackageByNameAndVersion(ctx.Repo.Repository.ID, models.PackageNuGet, packageName, packageVersion) + if err != nil { + if err == models.ErrPackageNotExist { + ctx.Error(http.StatusNotFound, "", err) + return + } + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + nugetPackage, err := intializePackage(p) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + resp := createRegistrationLeafResponse( + &linkBuilder{setting.AppURL + "api/v1/repos/" + ctx.Repo.Repository.FullName() + "/packages/nuget"}, + nugetPackage, + ) + + ctx.JSON(http.StatusOK, resp) +} + +// EnumeratePackageVersions https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions +func EnumeratePackageVersions(ctx *context.APIContext) { + packageName := ctx.Params("id") + + packages, err := models.GetPackagesByName(ctx.Repo.Repository.ID, models.PackageNuGet, packageName) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + if len(packages) == 0 { + ctx.Error(http.StatusNotFound, "", err) + return + } + + nugetPackages, err := intializePackages(packages) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + resp := createPackageVersionsResponse(nugetPackages) + + ctx.JSON(http.StatusOK, resp) +} + +// DownloadPackageContent https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg +func DownloadPackageContent(ctx *context.APIContext) { + packageName := ctx.Params("id") + packageVersion := ctx.Params("version") + filename := ctx.Params("filename") + + s, pf, err := package_service.GetPackageFileStream(ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion, filename) + if err != nil { + if err == models.ErrPackageNotExist { + ctx.Error(http.StatusNotFound, "", err) + return + } + ctx.Error(http.StatusInternalServerError, "", err) + return + } + defer s.Close() + + ctx.ServeStream(s, pf.Name) +} + +// UploadPackage creates a new package with the metadata contained in the uploaded nupgk file +// https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#push-a-package +func UploadPackage(ctx *context.APIContext) { + upload, err := ctx.UploadStream() + if err != nil { + ctx.Error(http.StatusBadRequest, "", err) + return + } + defer upload.Close() + + buf, err := filebuffer.CreateFromReader(upload, 32*1024*1024) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + defer buf.Close() + + meta, err := nuget_module.ParsePackageMetaData(buf, buf.Size()) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + p, err := package_service.CreatePackage( + ctx.User, + ctx.Repo.Repository, + models.PackageNuGet, + meta.ID, + meta.Version, + meta, + false, + ) + if err != nil { + if err == models.ErrDuplicatePackage { + ctx.Error(http.StatusBadRequest, "", err) + return + } + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + filename := strings.ToLower(fmt.Sprintf("%s.%s.nupkg", meta.ID, meta.Version)) + _, err = package_service.AddFileToPackage(p, filename, buf.Size(), buf) + if err != nil { + if err := models.DeletePackageByID(p.ID); err != nil { + log.Error("Error deleting package by id: %v", err) + } + ctx.Error(http.StatusInternalServerError, "", err) + } + + ctx.PlainText(http.StatusCreated, nil) +} + +// DeletePackage heard deletes the package +// https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#delete-a-package +func DeletePackage(ctx *context.APIContext) { + packageName := ctx.Params("id") + packageVersion := ctx.Params("version") + + err := package_service.DeletePackage(ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion) + if err != nil { + if err == models.ErrPackageNotExist { + ctx.Error(http.StatusNotFound, "", err) + return + } + ctx.Error(http.StatusInternalServerError, "", "") + } +} diff --git a/routers/api/v1/packages/nuget/package.go b/routers/api/v1/packages/nuget/package.go new file mode 100644 index 0000000000000..8480e28e29bb2 --- /dev/null +++ b/routers/api/v1/packages/nuget/package.go @@ -0,0 +1,65 @@ +// Copyright 2021 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 nuget + +import ( + "sort" + + "code.gitea.io/gitea/models" + nuget_module "code.gitea.io/gitea/modules/packages/nuget" + + "github.com/hashicorp/go-version" + jsoniter "github.com/json-iterator/go" +) + +// Package represents a package with NuGet metadata +type Package struct { + *models.Package + Metadata *nuget_module.Metadata +} + +func intializePackages(packages []*models.Package) ([]*Package, error) { + nugetPackages := make([]*Package, 0, len(packages)) + for _, p := range packages { + np, err := intializePackage(p) + if err != nil { + return nil, err + } + nugetPackages = append(nugetPackages, np) + } + return nugetPackages, nil +} + +func intializePackage(p *models.Package) (*Package, error) { + v, err := version.NewSemver(p.Version) + if err != nil { + return nil, err + } + p.SemVer = v + + var m *nuget_module.Metadata + err = jsoniter.Unmarshal([]byte(p.MetadataRaw), &m) + if err != nil { + return nil, err + } + if m == nil { + m = &nuget_module.Metadata{} + } + return &Package{ + p, + m, + }, nil +} + +func sortPackagesByVersionASC(packages []*Package) []*Package { + sortedPackages := make([]*Package, len(packages)) + copy(sortedPackages, packages) + + sort.Slice(sortedPackages, func(i, j int) bool { + return sortedPackages[i].SemVer.LessThan(sortedPackages[j].SemVer) + }) + + return sortedPackages +} diff --git a/services/packages/packages.go b/services/packages/packages.go index f9b4f18bbbf33..0cf0c11422bc3 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -11,18 +11,26 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" + + jsoniter "github.com/json-iterator/go" ) // CreatePackage creates a new package -func CreatePackage(creator *models.User, repository *models.Repository, packageType models.PackageType, name, version string, metaData interface{}, allowDuplicate bool) (*models.Package, error) { +func CreatePackage(creator *models.User, repository *models.Repository, packageType models.PackageType, name, version string, metadata interface{}, allowDuplicate bool) (*models.Package, error) { + metadataJSON, err := jsoniter.Marshal(metadata) + if err != nil { + log.Error("Error converting metadata to JSON: %v", err) + return nil, err + } + p := &models.Package{ - RepositoryID: repository.ID, - CreatorID: creator.ID, - Type: packageType, - Name: name, - LowerName: strings.ToLower(name), - Version: version, - MetaData: metaData, + RepoID: repository.ID, + CreatorID: creator.ID, + Type: packageType, + Name: name, + LowerName: strings.ToLower(name), + Version: version, + MetadataRaw: string(metadataJSON), } if err := models.TryInsertPackage(p); err != nil { if err == models.ErrDuplicatePackage { @@ -93,3 +101,27 @@ func DeletePackage(repository *models.Repository, packageType models.PackageType return nil } + +// GetPackageFileStream returns the content of the specific package file +func GetPackageFileStream(repository *models.Repository, packageType models.PackageType, name, version, filename string) (io.ReadCloser, *models.PackageFile, error) { + p, err := models.GetPackageByNameAndVersion(repository.ID, packageType, name, version) + if err != nil { + return nil, nil, err + } + + pfs, err := p.GetFiles() + if err != nil { + return nil, nil, err + } + + filename = strings.ToLower(filename) + + for _, pf := range pfs { + if pf.LowerName == filename { + s, err := packages_module.NewContentStore().Get(pf.ID) + return s, pf, err + } + } + + return nil, nil, models.ErrPackageNotExist +} From 2b9d96a55574c2109a1d99b3f4f007cbe6533387 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 13 Jul 2021 19:53:55 +0000 Subject: [PATCH 006/130] Moved service index to api file. --- routers/api/v1/packages/nuget/api.go | 28 ++++++++++++++++++++++++++ routers/api/v1/packages/nuget/nuget.go | 27 +++---------------------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/routers/api/v1/packages/nuget/api.go b/routers/api/v1/packages/nuget/api.go index f7586ab65b83c..c11d84bb83eef 100644 --- a/routers/api/v1/packages/nuget/api.go +++ b/routers/api/v1/packages/nuget/api.go @@ -12,6 +12,34 @@ import ( "github.com/hashicorp/go-version" ) +// ServiceIndexResponse https://docs.microsoft.com/en-us/nuget/api/service-index#resources +type ServiceIndexResponse struct { + Version string `json:"version"` + Resources []ServiceResource `json:"resources"` +} + +// ServiceResource https://docs.microsoft.com/en-us/nuget/api/service-index#resource +type ServiceResource struct { + ID string `json:"@id"` + Type string `json:"@type"` +} + +func createServiceIndexResponse(root string) *ServiceIndexResponse { + return &ServiceIndexResponse{ + Version: "3.0.0", + Resources: []ServiceResource{ + {ID: root + "/query", Type: "SearchQueryService"}, + {ID: root + "/query", Type: "SearchQueryService/3.0.0-beta"}, + {ID: root + "/query", Type: "SearchQueryService/3.0.0-rc"}, + {ID: root + "/registration", Type: "RegistrationsBaseUrl"}, + {ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-beta"}, + {ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-rc"}, + {ID: root + "/package", Type: "PackageBaseAddress/3.0.0"}, + {ID: root, Type: "PackagePublish/2.0.0"}, + }, + } +} + // RegistrationIndexResponse https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#response type RegistrationIndexResponse struct { RegistrationIndexURL string `json:"@id"` diff --git a/routers/api/v1/packages/nuget/nuget.go b/routers/api/v1/packages/nuget/nuget.go index 6cda36e77c975..c82751dacd02e 100644 --- a/routers/api/v1/packages/nuget/nuget.go +++ b/routers/api/v1/packages/nuget/nuget.go @@ -5,7 +5,6 @@ package nuget import ( - //"errors" "fmt" "net/http" "strings" @@ -24,29 +23,9 @@ import ( func ServiceIndex(ctx *context.APIContext) { repoName := ctx.Repo.Repository.FullName() - type resource struct { - ID string `json:"@id"` - Type string `json:"@type"` - } - - serviceIndex := struct { - Version string `json:"version"` - Resources []resource `json:"resources"` - }{ - Version: "3.0.0", - Resources: []resource{ - {ID: setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget/query", Type: "SearchQueryService"}, - {ID: setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget/query", Type: "SearchQueryService/3.0.0-beta"}, - {ID: setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget/query", Type: "SearchQueryService/3.0.0-rc"}, - {ID: setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget/registration", Type: "RegistrationsBaseUrl"}, - {ID: setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget/registration", Type: "RegistrationsBaseUrl/3.0.0-beta"}, - {ID: setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget/registration", Type: "RegistrationsBaseUrl/3.0.0-rc"}, - {ID: setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget/package", Type: "PackageBaseAddress/3.0.0"}, - {ID: setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget", Type: "PackagePublish/2.0.0"}, - }, - } - - ctx.JSON(http.StatusOK, serviceIndex) + resp := createServiceIndexResponse(setting.AppURL + "api/v1/repos/" + repoName + "/packages/nuget") + + ctx.JSON(http.StatusOK, resp) } // SearchService https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages From 99298b74744141ded1a7094f87ab783775281ab4 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 14 Jul 2021 23:14:00 +0200 Subject: [PATCH 007/130] Added NPM package registry. --- integrations/api_packages_npm_test.go | 118 ++++++++++++ integrations/api_packages_nuget_test.go | 52 ++++-- models/migrations/v188.go | 13 +- models/package.go | 28 ++- modules/packages/content_store.go | 16 +- modules/packages/npm/creator.go | 211 +++++++++++++++++++++ modules/packages/npm/creator_test.go | 224 +++++++++++++++++++++++ modules/packages/npm/metadata.go | 15 ++ modules/packages/nuget/metadata.go | 42 ++--- modules/packages/nuget/metadata_test.go | 24 +++ routers/api/v1/api.go | 8 + routers/api/v1/packages/npm/api.go | 61 ++++++ routers/api/v1/packages/npm/npm.go | 114 ++++++++++++ routers/api/v1/packages/npm/package.go | 74 ++++++++ routers/api/v1/packages/nuget/api.go | 23 ++- routers/api/v1/packages/nuget/nuget.go | 4 +- routers/api/v1/packages/nuget/package.go | 7 +- services/packages/packages.go | 38 +++- 18 files changed, 992 insertions(+), 80 deletions(-) create mode 100644 integrations/api_packages_npm_test.go create mode 100644 modules/packages/npm/creator.go create mode 100644 modules/packages/npm/creator_test.go create mode 100644 modules/packages/npm/metadata.go create mode 100644 routers/api/v1/packages/npm/api.go create mode 100644 routers/api/v1/packages/npm/npm.go create mode 100644 routers/api/v1/packages/npm/package.go diff --git a/integrations/api_packages_npm_test.go b/integrations/api_packages_npm_test.go new file mode 100644 index 0000000000000..a8097c90e4aa9 --- /dev/null +++ b/integrations/api_packages_npm_test.go @@ -0,0 +1,118 @@ +// Copyright 2017 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 ( + "encoding/base64" + "fmt" + "net/http" + "net/url" + "strings" + "testing" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/packages/npm" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func TestPackageNPM(t *testing.T) { + defer prepareTestEnv(t)() + repository := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 2}).(*models.Repository) + user := models.AssertExistsAndLoadBean(t, &models.User{ID: repository.OwnerID}).(*models.User) + + packageName := "@scope/test-package" + packageVersion := "1.0.1-pre" + packageAuthor := "KN4CK3R" + packageDescription := "Test Description" + + data := "H4sIAAAAAAAA/ytITM5OTE/VL4DQelnF+XkMVAYGBgZmJiYK2MRBwNDcSIHB2NTMwNDQzMwAqA7IMDUxA9LUdgg2UFpcklgEdAql5kD8ogCnhwio5lJQUMpLzE1VslJQcihOzi9I1S9JLS7RhSYIJR2QgrLUouLM/DyQGkM9Az1D3YIiqExKanFyUWZBCVQ2BKhVwQVJDKwosbQkI78IJO/tZ+LsbRykxFXLNdA+HwWjYBSMgpENACgAbtAACAAA" + upload := `{ + "_id": "` + packageName + `", + "name": "` + packageName + `", + "description": "` + packageDescription + `", + "versions": { + "` + packageVersion + `": { + "name": "` + packageName + `", + "version": "` + packageVersion + `", + "description": "` + packageDescription + `", + "author": { + "name": "` + packageAuthor + `" + }, + "dist": { + "integrity": "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==", + "shasum": "aaa7eaf852a948b0aa05afeda35b1badca155d90" + } + } + }, + "_attachments": { + "` + packageName + `-` + packageVersion + `.tgz": { + "data": "` + data + `" + } + } + }` + + root := fmt.Sprintf("/api/v1/repos/%s/%s/packages/npm/%s", user.Name, repository.Name, url.QueryEscape(packageName)) + filename := fmt.Sprintf("%s-%s.tgz", strings.Split(packageName, "/")[1], packageVersion) + + t.Run("Upload", func(t *testing.T) { + req := NewRequestWithBody(t, "PUT", root, strings.NewReader(upload)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusCreated) + + ps, err := models.GetPackagesByRepositoryID(repository.ID) + assert.NoError(t, err) + assert.Len(t, ps, 1) + assert.Equal(t, packageName, ps[0].Name) + assert.Equal(t, packageVersion, ps[0].Version) + + pfs, err := ps[0].GetFiles() + assert.NoError(t, err) + assert.Len(t, pfs, 1) + assert.Equal(t, filename, pfs[0].Name) + assert.Equal(t, int64(192), pfs[0].Size) + }) + + t.Run("UploadExists", func(t *testing.T) { + req := NewRequestWithBody(t, "PUT", root, strings.NewReader(upload)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusBadRequest) + }) + + t.Run("Download", func(t *testing.T) { + req := NewRequest(t, "GET", fmt.Sprintf("%s/-/%s/%s", root, packageVersion, filename)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + b, _ := base64.StdEncoding.DecodeString(data) + assert.Equal(t, b, resp.Body.Bytes()) + }) + + t.Run("PackageMetadata", func(t *testing.T) { + req := NewRequest(t, "GET", root) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + var result npm.PackageMetadata + DecodeJSON(t, resp, &result) + + assert.Equal(t, packageName, result.ID) + assert.Equal(t, packageName, result.Name) + assert.Equal(t, packageDescription, result.Description) + assert.Contains(t, result.DistTags, "latest") + assert.Equal(t, packageVersion, result.DistTags["latest"]) + assert.Equal(t, packageAuthor, result.Author.Name) + assert.Contains(t, result.Versions, packageVersion) + pmv := result.Versions[packageVersion] + assert.Equal(t, fmt.Sprintf("%s@%s", packageName, packageVersion), pmv.ID) + assert.Equal(t, packageName, pmv.Name) + assert.Equal(t, packageDescription, pmv.Description) + assert.Equal(t, packageAuthor, pmv.Author.Name) + assert.Equal(t, "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==", pmv.Dist.Integrity) + assert.Equal(t, "aaa7eaf852a948b0aa05afeda35b1badca155d90", pmv.Dist.Shasum) + assert.Equal(t, fmt.Sprintf("%s%s/-/%s/%s", setting.AppURL, root[1:], packageVersion, filename), pmv.Dist.Tarball) + }) +} diff --git a/integrations/api_packages_nuget_test.go b/integrations/api_packages_nuget_test.go index c5e08a96cc156..a31d87f96f0da 100644 --- a/integrations/api_packages_nuget_test.go +++ b/integrations/api_packages_nuget_test.go @@ -34,10 +34,10 @@ func TestPackageNuGet(t *testing.T) { w.Write([]byte(` - `+packageName+` - `+packageVersion+` - `+packageAuthors+` - `+packageDescription+` + ` + packageName + ` + ` + packageVersion + ` + ` + packageAuthors + ` + ` + packageDescription + ` `)) archive.Close() @@ -48,9 +48,35 @@ func TestPackageNuGet(t *testing.T) { t.Run("ServiceIndex", func(t *testing.T) { req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url)) req = AddBasicAuthHeader(req, user.Name) - _ = MakeRequest(t, req, http.StatusOK) + resp := MakeRequest(t, req, http.StatusOK) + + var result nuget.ServiceIndexResponse + DecodeJSON(t, resp, &result) - + assert.Equal(t, "3.0.0", result.Version) + assert.NotEmpty(t, result.Resources) + + root := setting.AppURL + url[1:] + for _, r := range result.Resources { + switch r.Type { + case "SearchQueryService": + fallthrough + case "SearchQueryService/3.0.0-beta": + fallthrough + case "SearchQueryService/3.0.0-rc": + assert.Equal(t, root+"/query", r.ID) + case "RegistrationsBaseUrl": + fallthrough + case "RegistrationsBaseUrl/3.0.0-beta": + fallthrough + case "RegistrationsBaseUrl/3.0.0-rc": + assert.Equal(t, root+"/registration", r.ID) + case "PackageBaseAddress/3.0.0": + assert.Equal(t, root+"/package", r.ID) + case "PackagePublish/2.0.0": + assert.Equal(t, root, r.ID) + } + } }) t.Run("Upload", func(t *testing.T) { @@ -86,13 +112,13 @@ func TestPackageNuGet(t *testing.T) { }) t.Run("SearchService", func(t *testing.T) { - cases := []struct{ - Query string - Skip int - Take int - ExpectedTotal int64 + cases := []struct { + Query string + Skip int + Take int + ExpectedTotal int64 ExpectedResults int - } { + }{ {"", 0, 0, 1, 1}, {"", 0, 10, 1, 1}, {"gitea", 0, 10, 0, 0}, @@ -112,7 +138,7 @@ func TestPackageNuGet(t *testing.T) { assert.Len(t, result.Data, c.ExpectedResults, "case %d: unexpected result count", i) } }) - + t.Run("RegistrationService", func(t *testing.T) { indexURL := fmt.Sprintf("%s%s/registration/%s/index.json", setting.AppURL, url[1:], packageName) leafURL := fmt.Sprintf("%s%s/registration/%s/%s.json", setting.AppURL, url[1:], packageName, packageVersion) diff --git a/models/migrations/v188.go b/models/migrations/v188.go index 5f8fb3aea52e5..48c2deae06414 100644 --- a/models/migrations/v188.go +++ b/models/migrations/v188.go @@ -30,11 +30,14 @@ func addPackageTables(x *xorm.Engine) error { } type PackageFile struct { - ID int64 `xorm:"pk autoincr"` - PackageID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` - Size int64 - Name string `xorm:"UNIQUE(s) NOT NULL"` - LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + ID int64 `xorm:"pk autoincr"` + PackageID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + Size int64 + Name string + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + HashSHA1 string `xorm:"hash_sha1"` + HashSHA256 string `xorm:"hash_sha256"` + HashSHA512 string `xorm:"hash_sha512"` CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated"` diff --git a/models/package.go b/models/package.go index 0ee54f91ef661..15f13bcb81cbe 100644 --- a/models/package.go +++ b/models/package.go @@ -10,7 +10,6 @@ import ( "code.gitea.io/gitea/modules/timeutil" - "github.com/hashicorp/go-version" "xorm.io/builder" ) @@ -21,6 +20,7 @@ type PackageType int const ( PackageGeneric PackageType = iota PackageNuGet // 1 + PackageNPM // 2 ) var ( @@ -37,10 +37,9 @@ type Package struct { CreatorID int64 Type PackageType `xorm:"UNIQUE(s) INDEX NOT NULL"` Name string - LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` - Version string `xorm:"UNIQUE(s) INDEX NOT NULL"` - SemVer *version.Version `xorm:"-"` - MetadataRaw string `xorm:"TEXT"` + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + Version string `xorm:"UNIQUE(s) INDEX NOT NULL"` + MetadataRaw string `xorm:"TEXT"` CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated"` @@ -48,11 +47,14 @@ type Package struct { // PackageFile represents files associated with a package type PackageFile struct { - ID int64 `xorm:"pk autoincr"` - PackageID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` - Size int64 - Name string `xorm:"UNIQUE(s) NOT NULL"` - LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + ID int64 `xorm:"pk autoincr"` + PackageID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + Size int64 + Name string + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + HashSHA1 string `xorm:"hash_sha1"` + HashSHA256 string `xorm:"hash_sha256"` + HashSHA512 string `xorm:"hash_sha512"` CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated"` @@ -175,6 +177,12 @@ func InsertPackageFile(pf *PackageFile) error { return err } +// UpdatePackageFile updates a package file +func UpdatePackageFile(pf *PackageFile) error { + _, err := x.ID(pf.ID).Update(pf) + return err +} + // DeletePackageFileByID deletes a package file func DeletePackageFileByID(fileID int64) error { _, err := x.ID(fileID).Delete(&PackageFile{}) diff --git a/modules/packages/content_store.go b/modules/packages/content_store.go index 5c22c8eaf5119..c1bfb99f36d69 100644 --- a/modules/packages/content_store.go +++ b/modules/packages/content_store.go @@ -23,21 +23,21 @@ func NewContentStore() *ContentStore { } // Get get the package file content -func (s *ContentStore) Get(packageFileID int64) (storage.Object, error) { - return s.store.Open(toRelativePath(packageFileID)) +func (s *ContentStore) Get(packageID, packageFileID int64) (storage.Object, error) { + return s.store.Open(toRelativePath(packageID, packageFileID)) } // Save stores the package file content -func (s *ContentStore) Save(packageFileID int64, r io.Reader, size int64) error { - _, err := s.store.Save(toRelativePath(packageFileID), r, size) +func (s *ContentStore) Save(packageID, packageFileID int64, r io.Reader, size int64) error { + _, err := s.store.Save(toRelativePath(packageID, packageFileID), r, size) return err } // Delete deletes the package file content -func (s *ContentStore) Delete(packageFileID int64) error { - return s.store.Delete(toRelativePath(packageFileID)) +func (s *ContentStore) Delete(packageID, packageFileID int64) error { + return s.store.Delete(toRelativePath(packageID, packageFileID)) } -func toRelativePath(packageFileID int64) string { - return fmt.Sprintf("%d/package", packageFileID) +func toRelativePath(packageID, packageFileID int64) string { + return fmt.Sprintf("%d/%d", packageID, packageFileID) } diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go new file mode 100644 index 0000000000000..b0766f7a92e15 --- /dev/null +++ b/modules/packages/npm/creator.go @@ -0,0 +1,211 @@ +// Copyright 2021 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 npm + +import ( + "bytes" + "crypto/sha1" + "crypto/sha512" + "encoding/base64" + "errors" + "fmt" + "io" + "regexp" + "strings" + "time" + + "github.com/hashicorp/go-version" + jsoniter "github.com/json-iterator/go" +) + +var ( + // ErrInvalidPackage indicates an invalid package + ErrInvalidPackage = errors.New("The package is invalid") + // ErrInvalidPackageName indicates an invalid name + ErrInvalidPackageName = errors.New("The package name is invalid") + // ErrInvalidPackageVersion indicates an invalid version + ErrInvalidPackageVersion = errors.New("The package version is invalid") + // ErrInvalidAttachment indicates a invalid attachment + ErrInvalidAttachment = errors.New("The package attachment is invalid") + // ErrInvalidIntegrity indicates an integrity validation error + ErrInvalidIntegrity = errors.New("Failed to validate integrity") +) + +var nameMatch = regexp.MustCompile(`\A(?:@([^/~'!\(\)\*]+?)[/])?([^/~'!\(\)\*]+?)\z`) + +// Package represents a NPM package +type Package struct { + Name string + Version string + Metadata Metadata + Filename string + Data []byte +} + +// PackageMetadata https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package +type PackageMetadata struct { + ID string `json:"_id"` + Name string `json:"name"` + Description string `json:"description"` + DistTags map[string]string `json:"dist-tags,omitempty"` + Versions map[string]*PackageMetadataVersion `json:"versions"` + Readme string `json:"readme,omitempty"` + Maintainers []User `json:"maintainers,omitempty"` + Time map[string]time.Time `json:"time,omitempty"` + Homepage string `json:"homepage,omitempty"` + Keywords []string `json:"keywords,omitempty"` + Repository Repository `json:"repository,omitempty"` + Author User `json:"author"` + ReadmeFilename string `json:"readmeFilename,omitempty"` + Users map[string]bool `json:"users,omitempty"` + License string `json:"license,omitempty"` +} + +// PackageMetadataVersion https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version +type PackageMetadataVersion struct { + ID string `json:"_id"` + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Author User `json:"author"` + Homepage string `json:"homepage,omitempty"` + License string `json:"license,omitempty"` + Repository Repository `json:"repository,omitempty"` + Keywords []string `json:"keywords,omitempty"` + Dependencies map[string]string `json:"dependencies,omitempty"` + DevDependencies map[string]string `json:"devDependencies,omitempty"` + Readme string `json:"readme,omitempty"` + Dist PackageDistribution `json:"dist"` + Maintainers []User `json:"maintainers,omitempty"` +} + +// PackageDistribution https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version +type PackageDistribution struct { + Integrity string `json:"integrity"` + Shasum string `json:"shasum"` + Tarball string `json:"tarball"` + FileCount int `json:"fileCount,omitempty"` + UnpackedSize int `json:"unpackedSize,omitempty"` + NpmSignature string `json:"npm-signature,omitempty"` +} + +// User https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package +type User struct { + Username string `json:"username,omitempty"` + Name string `json:"name"` + Email string `json:"email,omitempty"` + URL string `json:"url,omitempty"` +} + +// Repository https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version +type Repository struct { + Type string `json:"type"` + URL string `json:"url"` +} + +// PackageAttachment https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package +type PackageAttachment struct { + ContentType string `json:"content_type"` + Data string `json:"data"` + Length int `json:"length"` +} + +type packageUpload struct { + PackageMetadata + Attachments map[string]*PackageAttachment `json:"_attachments"` +} + +// ParsePackage parses the content into a NPM package +func ParsePackage(r io.Reader) (*Package, error) { + var upload packageUpload + if err := jsoniter.NewDecoder(r).Decode(&upload); err != nil { + return nil, err + } + + for key, meta := range upload.Versions { + if !validateName(meta.Name) { + return nil, ErrInvalidPackageName + } + + _, err := version.NewSemver(key) + if err != nil { + return nil, ErrInvalidPackageVersion + } + + p := &Package{ + Name: meta.Name, + Version: meta.Version, + Metadata: Metadata{ + Description: meta.Description, + Author: meta.Author.Name, + License: meta.License, + Homepage: meta.Homepage, + Dependencies: meta.Dependencies, + Readme: meta.Readme, + }, + } + + names := strings.SplitN(p.Name, "/", 2) + name := names[0] + if len(names) == 2 { + name = names[1] + } + p.Filename = strings.ToLower(fmt.Sprintf("%s-%s.tgz", name, p.Version)) + + attachment := func() *PackageAttachment { + for _, a := range upload.Attachments { + return a + } + return nil + }() + if attachment == nil || len(attachment.Data) == 0 { + return nil, ErrInvalidAttachment + } + + data, err := base64.StdEncoding.DecodeString(attachment.Data) + if err != nil { + return nil, ErrInvalidAttachment + } + p.Data = data + + integrity := strings.SplitN(meta.Dist.Integrity, "-", 2) + if len(integrity) != 2 { + return nil, ErrInvalidIntegrity + } + integrityHash, err := base64.StdEncoding.DecodeString(integrity[1]) + if err != nil { + return nil, ErrInvalidIntegrity + } + var hash []byte + switch integrity[0] { + case "sha1": + tmp := sha1.Sum(data) + hash = tmp[:] + case "sha512": + tmp := sha512.Sum512(data) + hash = tmp[:] + } + if !bytes.Equal(integrityHash, hash) { + return nil, ErrInvalidIntegrity + } + + return p, nil + } + + return nil, ErrInvalidPackage +} + +func validateName(name string) bool { + if strings.TrimSpace(name) != name { + return false + } + if len(name) == 0 || len(name) > 214 { + return false + } + if name[0] == '.' || name[0] == '_' { + return false + } + return nameMatch.MatchString(name) +} diff --git a/modules/packages/npm/creator_test.go b/modules/packages/npm/creator_test.go new file mode 100644 index 0000000000000..c777bbac91d36 --- /dev/null +++ b/modules/packages/npm/creator_test.go @@ -0,0 +1,224 @@ +// Copyright 2021 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 npm + +import ( + "bytes" + "encoding/base64" + "fmt" + "strings" + "testing" + + jsoniter "github.com/json-iterator/go" + "github.com/stretchr/testify/assert" +) + +func TestParsePackage(t *testing.T) { + packageName := "@scope/test-package" + packageVersion := "1.0.1-pre" + packageAuthor := "KN4CK3R" + packageDescription := "Test Description" + data := "H4sIAAAAAAAA/ytITM5OTE/VL4DQelnF+XkMVAYGBgZmJiYK2MRBwNDcSIHB2NTMwNDQzMwAqA7IMDUxA9LUdgg2UFpcklgEdAql5kD8ogCnhwio5lJQUMpLzE1VslJQcihOzi9I1S9JLS7RhSYIJR2QgrLUouLM/DyQGkM9Az1D3YIiqExKanFyUWZBCVQ2BKhVwQVJDKwosbQkI78IJO/tZ+LsbRykxFXLNdA+HwWjYBSMgpENACgAbtAACAAA" + integrity := "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==" + + t.Run("InvalidUpload", func(t *testing.T) { + p, err := ParsePackage(bytes.NewReader([]byte{0})) + assert.Nil(t, p) + assert.Error(t, err) + }) + + t.Run("InvalidUploadNoData", func(t *testing.T) { + b, _ := jsoniter.Marshal(packageUpload{}) + p, err := ParsePackage(bytes.NewReader(b)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidPackage) + }) + + t.Run("InvalidPackageName", func(t *testing.T) { + name := " test " + b, _ := jsoniter.Marshal(packageUpload{ + PackageMetadata: PackageMetadata{ + ID: name, + Name: name, + Versions: map[string]*PackageMetadataVersion{ + packageVersion: { + Name: name, + }, + }, + }, + }) + + p, err := ParsePackage(bytes.NewReader(b)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidPackageName) + }) + + t.Run("InvalidPackageVersion", func(t *testing.T) { + version := "first-version" + b, _ := jsoniter.Marshal(packageUpload{ + PackageMetadata: PackageMetadata{ + ID: packageName, + Name: packageName, + Versions: map[string]*PackageMetadataVersion{ + version: { + Name: packageName, + }, + }, + }, + }) + + p, err := ParsePackage(bytes.NewReader(b)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidPackageVersion) + }) + + t.Run("InvalidAttachment", func(t *testing.T) { + b, _ := jsoniter.Marshal(packageUpload{ + PackageMetadata: PackageMetadata{ + ID: packageName, + Name: packageName, + Versions: map[string]*PackageMetadataVersion{ + packageVersion: { + Name: packageName, + }, + }, + }, + Attachments: map[string]*PackageAttachment{ + "dummy.tgz": {}, + }, + }) + + p, err := ParsePackage(bytes.NewReader(b)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidAttachment) + }) + + t.Run("InvalidData", func(t *testing.T) { + filename := fmt.Sprintf("%s-%s.tgz", packageName, packageVersion) + b, _ := jsoniter.Marshal(packageUpload{ + PackageMetadata: PackageMetadata{ + ID: packageName, + Name: packageName, + Versions: map[string]*PackageMetadataVersion{ + packageVersion: { + Name: packageName, + }, + }, + }, + Attachments: map[string]*PackageAttachment{ + filename: { + Data: "/", + }, + }, + }) + + p, err := ParsePackage(bytes.NewReader(b)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidAttachment) + }) + + t.Run("InvalidIntegrity", func(t *testing.T) { + filename := fmt.Sprintf("%s-%s.tgz", packageName, packageVersion) + b, _ := jsoniter.Marshal(packageUpload{ + PackageMetadata: PackageMetadata{ + ID: packageName, + Name: packageName, + Versions: map[string]*PackageMetadataVersion{ + packageVersion: { + Name: packageName, + Dist: PackageDistribution{ + Integrity: "sha512-test==", + }, + }, + }, + }, + Attachments: map[string]*PackageAttachment{ + filename: { + Data: data, + }, + }, + }) + + p, err := ParsePackage(bytes.NewReader(b)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidIntegrity) + }) + + t.Run("InvalidIntegrity2", func(t *testing.T) { + filename := fmt.Sprintf("%s-%s.tgz", packageName, packageVersion) + b, _ := jsoniter.Marshal(packageUpload{ + PackageMetadata: PackageMetadata{ + ID: packageName, + Name: packageName, + Versions: map[string]*PackageMetadataVersion{ + packageVersion: { + Name: packageName, + Dist: PackageDistribution{ + Integrity: integrity, + }, + }, + }, + }, + Attachments: map[string]*PackageAttachment{ + filename: { + Data: base64.StdEncoding.EncodeToString([]byte("data")), + }, + }, + }) + + p, err := ParsePackage(bytes.NewReader(b)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidIntegrity) + }) + + t.Run("Valid", func(t *testing.T) { + filename := fmt.Sprintf("%s-%s.tgz", packageName, packageVersion) + b, _ := jsoniter.Marshal(packageUpload{ + PackageMetadata: PackageMetadata{ + ID: packageName, + Name: packageName, + Versions: map[string]*PackageMetadataVersion{ + packageVersion: { + Name: packageName, + Version: packageVersion, + Description: packageDescription, + Author: User{Name: packageAuthor}, + License: "MIT", + Homepage: "https://gitea.io/", + Readme: packageDescription, + Dependencies: map[string]string{ + "package": "1.2.0", + }, + Dist: PackageDistribution{ + Integrity: integrity, + }, + }, + }, + }, + Attachments: map[string]*PackageAttachment{ + filename: { + Data: data, + }, + }, + }) + + p, err := ParsePackage(bytes.NewReader(b)) + assert.NotNil(t, p) + assert.NoError(t, err) + + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageVersion, p.Version) + assert.Equal(t, fmt.Sprintf("%s-%s.tgz", strings.Split(packageName, "/")[1], packageVersion), p.Filename) + b, _ = base64.StdEncoding.DecodeString(data) + assert.Equal(t, b, p.Data) + assert.Equal(t, packageDescription, p.Metadata.Description) + assert.Equal(t, packageDescription, p.Metadata.Readme) + assert.Equal(t, packageAuthor, p.Metadata.Author) + assert.Equal(t, "MIT", p.Metadata.License) + assert.Equal(t, "https://gitea.io/", p.Metadata.Homepage) + assert.Contains(t, p.Metadata.Dependencies, "package") + assert.Equal(t, "1.2.0", p.Metadata.Dependencies["package"]) + }) +} diff --git a/modules/packages/npm/metadata.go b/modules/packages/npm/metadata.go new file mode 100644 index 0000000000000..f580c868e9e6d --- /dev/null +++ b/modules/packages/npm/metadata.go @@ -0,0 +1,15 @@ +// Copyright 2021 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 npm + +// Metadata represents the metadata of a NPM package +type Metadata struct { + Description string `json:"description"` + Author string `json:"author"` + License string `json:"license"` + Homepage string `json:"homepage"` + Dependencies map[string]string `json:"dependencies"` + Readme string `json:"readme"` +} diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index 6ca2de0d9cc95..f67ac78c4ed6c 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -33,22 +33,21 @@ const maxNuspecFileSize = 3 * 1024 * 1024 // Metadata represents the metadata of a Nuget package type Metadata struct { - ID string `json:"-"` - Version string `json:"-"` - Description string - Summary string - ReleaseNotes string - Authors string - RequireLicenseAcceptance bool - ProjectURL string `json:"project_url"` - RepositoryURL string `json:"repository_url"` - Dependencies map[string][]Dependency + ID string `json:"-"` + Version string `json:"-"` + Description string `json:"description"` + Summary string `json:"summary"` + ReleaseNotes string `json:"release_notes"` + Authors string `json:"authors"` + ProjectURL string `json:"project_url"` + RepositoryURL string `json:"repository_url"` + Dependencies map[string][]Dependency `json:"dependencies"` } // Dependency represents a dependency of a Nuget package type Dependency struct { - ID string - Version string + ID string `json:"id"` + Version string `json:"version"` } type nuspecPackage struct { @@ -123,16 +122,15 @@ func ParseNuspecMetaData(r io.Reader) (*Metadata, error) { } m := &Metadata{ - ID: p.Metadata.ID, - Version: v.String(), - Description: p.Metadata.Description, - Summary: p.Metadata.Summary, - ReleaseNotes: p.Metadata.ReleaseNotes, - Authors: p.Metadata.Authors, - RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance, - ProjectURL: p.Metadata.ProjectURL, - RepositoryURL: p.Metadata.Repository.URL, - Dependencies: make(map[string][]Dependency), + ID: p.Metadata.ID, + Version: v.String(), + Description: p.Metadata.Description, + Summary: p.Metadata.Summary, + ReleaseNotes: p.Metadata.ReleaseNotes, + Authors: p.Metadata.Authors, + ProjectURL: p.Metadata.ProjectURL, + RepositoryURL: p.Metadata.Repository.URL, + Dependencies: make(map[string][]Dependency), } for _, group := range p.Metadata.Dependencies.Group { deps := make([]Dependency, 0, len(group.Dependency)) diff --git a/modules/packages/nuget/metadata_test.go b/modules/packages/nuget/metadata_test.go index 87c73c53ebfe2..b72b282bab8c3 100644 --- a/modules/packages/nuget/metadata_test.go +++ b/modules/packages/nuget/metadata_test.go @@ -76,11 +76,35 @@ func TestParsePackageMetaData(t *testing.T) { t.Run("InvalidNuspecFile", func(t *testing.T) { data := createArchive("package.nuspec", "") + m, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data))) + assert.Nil(t, m) + assert.Error(t, err) + }) + + t.Run("InvalidPackageId", func(t *testing.T) { + data := createArchive("package.nuspec", ` + + + `) + m, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data))) assert.Nil(t, m) assert.ErrorIs(t, err, ErrNuspecInvalidID) }) + t.Run("InvalidPackageVersion", func(t *testing.T) { + data := createArchive("package.nuspec", ` + + + `+id+` + + `) + + m, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data))) + assert.Nil(t, m) + assert.ErrorIs(t, err, ErrNuspecInvalidVersion) + }) + t.Run("Valid", func(t *testing.T) { data := createArchive("package.nuspec", nuspecContent) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index ae64cb3b3b8ad..1b8907b90be28 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -80,6 +80,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/notify" "code.gitea.io/gitea/routers/api/v1/org" "code.gitea.io/gitea/routers/api/v1/packages/generic" + "code.gitea.io/gitea/routers/api/v1/packages/npm" "code.gitea.io/gitea/routers/api/v1/packages/nuget" "code.gitea.io/gitea/routers/api/v1/repo" "code.gitea.io/gitea/routers/api/v1/settings" @@ -997,6 +998,13 @@ func Routes() *web.Route { }) }) }, reqBasicAuth()) + m.Group("/npm", func() { + m.Group("/{id}", func() { + m.Get("", npm.PackageMetadata) + m.Put("" /*reqRepoWriter(models.UnitTypePackage),*/, npm.UploadPackage) + m.Get("/-/{version}/{filename}", npm.DownloadPackageContent) + }) + }, reqToken()) }) }, repoAssignment()) }) diff --git a/routers/api/v1/packages/npm/api.go b/routers/api/v1/packages/npm/api.go new file mode 100644 index 0000000000000..a916d97747519 --- /dev/null +++ b/routers/api/v1/packages/npm/api.go @@ -0,0 +1,61 @@ +// Copyright 2021 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 npm + +import ( + "encoding/base64" + "encoding/hex" + "fmt" + "net/url" + + npm_module "code.gitea.io/gitea/modules/packages/npm" +) + +func createPackageMetadataResponse(registryURL string, packages []*Package) *npm_module.PackageMetadata { + sortedPackages := sortPackagesByVersionASC(packages) + + versions := make(map[string]*npm_module.PackageMetadataVersion) + for _, p := range sortedPackages { + versions[p.SemVer.String()] = createPackageMetadataVersion(registryURL, p) + } + + latest := sortedPackages[len(sortedPackages)-1] + + distTags := make(map[string]string) + distTags["latest"] = latest.Version + + return &npm_module.PackageMetadata{ + ID: latest.Package.Name, + Name: latest.Package.Name, + DistTags: distTags, + Description: latest.Metadata.Description, + Readme: latest.Metadata.Readme, + Homepage: latest.Metadata.Homepage, + Author: npm_module.User{Name: latest.Metadata.Author}, + License: latest.Metadata.License, + Versions: versions, + } +} + +func createPackageMetadataVersion(registryURL string, p *Package) *npm_module.PackageMetadataVersion { + hashBytes, _ := hex.DecodeString(p.PackageFile.HashSHA512) + + return &npm_module.PackageMetadataVersion{ + ID: fmt.Sprintf("%s@%s", p.Package.Name, p.Package.Version), + Name: p.Package.Name, + Version: p.Package.Version, + Description: p.Metadata.Description, + Author: npm_module.User{Name: p.Metadata.Author}, + Homepage: p.Metadata.Homepage, + License: p.Metadata.License, + Dependencies: p.Metadata.Dependencies, + Readme: p.Metadata.Readme, + Dist: npm_module.PackageDistribution{ + Shasum: p.PackageFile.HashSHA1, + Integrity: "sha512-" + base64.StdEncoding.EncodeToString(hashBytes), + Tarball: fmt.Sprintf("%s/%s/-/%s/%s", registryURL, url.QueryEscape(p.Package.Name), p.Package.Version, p.PackageFile.LowerName), + }, + } +} diff --git a/routers/api/v1/packages/npm/npm.go b/routers/api/v1/packages/npm/npm.go new file mode 100644 index 0000000000000..99caee7c31ff1 --- /dev/null +++ b/routers/api/v1/packages/npm/npm.go @@ -0,0 +1,114 @@ +// Copyright 2021 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 npm + +import ( + "bytes" + "net/http" + "net/url" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + npm_module "code.gitea.io/gitea/modules/packages/npm" + "code.gitea.io/gitea/modules/setting" + + package_service "code.gitea.io/gitea/services/packages" +) + +// PackageMetadata returns the metadata for a single package +func PackageMetadata(ctx *context.APIContext) { + packageName, err := url.QueryUnescape(ctx.Params("id")) + if err != nil { + ctx.Error(http.StatusBadRequest, "", err) + return + } + + packages, err := models.GetPackagesByName(ctx.Repo.Repository.ID, models.PackageNPM, packageName) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + if len(packages) == 0 { + ctx.Error(http.StatusNotFound, "", err) + return + } + + npmPackages, err := intializePackages(packages) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + resp := createPackageMetadataResponse( + setting.AppURL+"api/v1/repos/"+ctx.Repo.Repository.FullName()+"/packages/npm", + npmPackages, + ) + + ctx.JSON(http.StatusOK, resp) +} + +// DownloadPackageContent serves the content of a package +func DownloadPackageContent(ctx *context.APIContext) { + packageName, err := url.QueryUnescape(ctx.Params("id")) + if err != nil { + ctx.Error(http.StatusBadRequest, "", err) + return + } + packageVersion := ctx.Params("version") + filename := ctx.Params("filename") + + s, pf, err := package_service.GetPackageFileStream(ctx.Repo.Repository, models.PackageNPM, packageName, packageVersion, filename) + if err != nil { + if err == models.ErrPackageNotExist { + ctx.Error(http.StatusNotFound, "", err) + return + } + ctx.Error(http.StatusInternalServerError, "", err) + return + } + defer s.Close() + + ctx.ServeStream(s, pf.Name) +} + +// UploadPackage creates a new package +func UploadPackage(ctx *context.APIContext) { + defer ctx.Req.Body.Close() + + npmPackage, err := npm_module.ParsePackage(ctx.Req.Body) + if err != nil { + ctx.Error(http.StatusBadRequest, "", err) + return + } + + p, err := package_service.CreatePackage( + ctx.User, + ctx.Repo.Repository, + models.PackageNPM, + npmPackage.Name, + npmPackage.Version, + npmPackage.Metadata, + false, + ) + if err != nil { + if err == models.ErrDuplicatePackage { + ctx.Error(http.StatusBadRequest, "", err) + return + } + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + _, err = package_service.AddFileToPackage(p, npmPackage.Filename, int64(len(npmPackage.Data)), bytes.NewReader(npmPackage.Data)) + if err != nil { + if err := models.DeletePackageByID(p.ID); err != nil { + log.Error("Error deleting package by id: %v", err) + } + ctx.Error(http.StatusInternalServerError, "", err) + } + + ctx.PlainText(http.StatusCreated, nil) +} diff --git a/routers/api/v1/packages/npm/package.go b/routers/api/v1/packages/npm/package.go new file mode 100644 index 0000000000000..aa845c58b5a81 --- /dev/null +++ b/routers/api/v1/packages/npm/package.go @@ -0,0 +1,74 @@ +// Copyright 2021 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 npm + +import ( + "sort" + + "code.gitea.io/gitea/models" + npm_module "code.gitea.io/gitea/modules/packages/npm" + + "github.com/hashicorp/go-version" + jsoniter "github.com/json-iterator/go" +) + +// Package represents a package with NPM metadata +type Package struct { + *models.Package + *models.PackageFile + SemVer *version.Version + Metadata *npm_module.Metadata +} + +func intializePackages(packages []*models.Package) ([]*Package, error) { + nugetPackages := make([]*Package, 0, len(packages)) + for _, p := range packages { + np, err := intializePackage(p) + if err != nil { + return nil, err + } + nugetPackages = append(nugetPackages, np) + } + return nugetPackages, nil +} + +func intializePackage(p *models.Package) (*Package, error) { + v, err := version.NewSemver(p.Version) + if err != nil { + return nil, err + } + + var m *npm_module.Metadata + err = jsoniter.Unmarshal([]byte(p.MetadataRaw), &m) + if err != nil { + return nil, err + } + if m == nil { + m = &npm_module.Metadata{} + } + + pfs, err := p.GetFiles() + if err != nil { + return nil, err + } + + return &Package{ + Package: p, + PackageFile: pfs[0], + SemVer: v, + Metadata: m, + }, nil +} + +func sortPackagesByVersionASC(packages []*Package) []*Package { + sortedPackages := make([]*Package, len(packages)) + copy(sortedPackages, packages) + + sort.Slice(sortedPackages, func(i, j int) bool { + return sortedPackages[i].SemVer.LessThan(sortedPackages[j].SemVer) + }) + + return sortedPackages +} diff --git a/routers/api/v1/packages/nuget/api.go b/routers/api/v1/packages/nuget/api.go index c11d84bb83eef..b981cc98256b7 100644 --- a/routers/api/v1/packages/nuget/api.go +++ b/routers/api/v1/packages/nuget/api.go @@ -14,7 +14,7 @@ import ( // ServiceIndexResponse https://docs.microsoft.com/en-us/nuget/api/service-index#resources type ServiceIndexResponse struct { - Version string `json:"version"` + Version string `json:"version"` Resources []ServiceResource `json:"resources"` } @@ -120,17 +120,16 @@ func createRegistrationIndexPageItem(l *linkBuilder, p *Package) *RegistrationIn RegistrationLeafURL: l.GetRegistrationLeafURL(p.Name, p.Version), PackageContentURL: l.GetPackageDownloadURL(p.Name, p.Version), CatalogEntry: &CatalogEntry{ - CatalogLeafURL: l.GetRegistrationLeafURL(p.Name, p.Version), - PackageContentURL: l.GetPackageDownloadURL(p.Name, p.Version), - ID: p.Name, - Version: p.Version, - Description: p.Metadata.Description, - Summary: p.Metadata.Summary, - ReleaseNotes: p.Metadata.ReleaseNotes, - Authors: p.Metadata.Authors, - RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance, - ProjectURL: p.Metadata.ProjectURL, - DependencyGroups: createDependencyGroups(p), + CatalogLeafURL: l.GetRegistrationLeafURL(p.Name, p.Version), + PackageContentURL: l.GetPackageDownloadURL(p.Name, p.Version), + ID: p.Name, + Version: p.Version, + Description: p.Metadata.Description, + Summary: p.Metadata.Summary, + ReleaseNotes: p.Metadata.ReleaseNotes, + Authors: p.Metadata.Authors, + ProjectURL: p.Metadata.ProjectURL, + DependencyGroups: createDependencyGroups(p), }, } } diff --git a/routers/api/v1/packages/nuget/nuget.go b/routers/api/v1/packages/nuget/nuget.go index c82751dacd02e..f3cde14ce5eb4 100644 --- a/routers/api/v1/packages/nuget/nuget.go +++ b/routers/api/v1/packages/nuget/nuget.go @@ -143,7 +143,7 @@ func DownloadPackageContent(ctx *context.APIContext) { packageVersion := ctx.Params("version") filename := ctx.Params("filename") - s, pf, err := package_service.GetPackageFileStream(ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion, filename) + s, pf, err := package_service.GetPackageFileStream(ctx.Repo.Repository, models.PackageNuGet, packageName, packageVersion, filename) if err != nil { if err == models.ErrPackageNotExist { ctx.Error(http.StatusNotFound, "", err) @@ -210,7 +210,7 @@ func UploadPackage(ctx *context.APIContext) { ctx.PlainText(http.StatusCreated, nil) } -// DeletePackage heard deletes the package +// DeletePackage hard deletes the package // https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#delete-a-package func DeletePackage(ctx *context.APIContext) { packageName := ctx.Params("id") diff --git a/routers/api/v1/packages/nuget/package.go b/routers/api/v1/packages/nuget/package.go index 8480e28e29bb2..442b134321c18 100644 --- a/routers/api/v1/packages/nuget/package.go +++ b/routers/api/v1/packages/nuget/package.go @@ -17,6 +17,7 @@ import ( // Package represents a package with NuGet metadata type Package struct { *models.Package + SemVer *version.Version Metadata *nuget_module.Metadata } @@ -37,7 +38,6 @@ func intializePackage(p *models.Package) (*Package, error) { if err != nil { return nil, err } - p.SemVer = v var m *nuget_module.Metadata err = jsoniter.Unmarshal([]byte(p.MetadataRaw), &m) @@ -48,8 +48,9 @@ func intializePackage(p *models.Package) (*Package, error) { m = &nuget_module.Metadata{} } return &Package{ - p, - m, + Package: p, + SemVer: v, + Metadata: m, }, nil } diff --git a/services/packages/packages.go b/services/packages/packages.go index 0cf0c11422bc3..b75b2542425e1 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -5,6 +5,10 @@ package packages import ( + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "fmt" "io" "strings" @@ -58,9 +62,33 @@ func AddFileToPackage(p *models.Package, filename string, size int64, r io.Reade return nil, err } - packageStore := packages_module.NewContentStore() - if err := packageStore.Save(pf.ID, r, size); err != nil { - log.Error("Error saving package file: %v", err) + h1 := sha1.New() + h256 := sha256.New() + h512 := sha512.New() + + r = io.TeeReader(r, io.MultiWriter(h1, h256, h512)) + + contentStore := packages_module.NewContentStore() + + err := func() error { + err := contentStore.Save(p.ID, pf.ID, r, size) + if err != nil { + log.Error("Error saving package file in content store: %v", err) + return err + } + + pf.HashSHA1 = fmt.Sprintf("%x", h1.Sum(nil)) + pf.HashSHA256 = fmt.Sprintf("%x", h256.Sum(nil)) + pf.HashSHA512 = fmt.Sprintf("%x", h512.Sum(nil)) + if err = models.UpdatePackageFile(pf); err != nil { + log.Error("Error updating package file: %v", err) + return err + } + return nil + }() + if err != nil { + _ = contentStore.Delete(p.ID, pf.ID) + if err := models.DeletePackageFileByID(pf.ID); err != nil { log.Error("Error deleting package file: %v", err) } @@ -88,7 +116,7 @@ func DeletePackage(repository *models.Repository, packageType models.PackageType contentStore := packages_module.NewContentStore() for _, pf := range pfs { - if err := contentStore.Delete(pf.ID); err != nil { + if err := contentStore.Delete(p.ID, pf.ID); err != nil { log.Error("Error deleting package file: %v", err) return err } @@ -118,7 +146,7 @@ func GetPackageFileStream(repository *models.Repository, packageType models.Pack for _, pf := range pfs { if pf.LowerName == filename { - s, err := packages_module.NewContentStore().Get(pf.ID) + s, err := packages_module.NewContentStore().Get(p.ID, pf.ID) return s, pf, err } } From c7fb9329642298f6c5c73d79690b160c952e5edd Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sat, 17 Jul 2021 12:06:22 +0000 Subject: [PATCH 008/130] Added Maven package registry. --- integrations/api_packages_generic_test.go | 4 +- integrations/api_packages_maven_test.go | 106 ++++++++ integrations/api_packages_npm_test.go | 2 +- integrations/api_packages_nuget_test.go | 4 +- models/migrations/v188.go | 1 + models/package.go | 90 +++++-- modules/context/context.go | 14 +- modules/packages/maven/metadata.go | 83 ++++++ modules/packages/maven/metadata_test.go | 73 +++++ modules/packages/npm/creator.go | 2 +- modules/packages/npm/creator_test.go | 2 +- modules/packages/npm/metadata.go | 2 +- modules/packages/nuget/metadata.go | 4 +- modules/packages/nuget/metadata_test.go | 4 +- routers/api/v1/api.go | 5 + routers/api/v1/packages/generic/generic.go | 8 +- routers/api/v1/packages/maven/api.go | 36 +++ routers/api/v1/packages/maven/maven.go | 295 +++++++++++++++++++++ routers/api/v1/packages/maven/package.go | 59 +++++ routers/api/v1/packages/npm/api.go | 4 +- routers/api/v1/packages/npm/npm.go | 4 +- routers/api/v1/packages/npm/package.go | 6 +- routers/api/v1/packages/nuget/nuget.go | 13 +- routers/api/v1/packages/nuget/package.go | 6 +- services/packages/packages.go | 39 +-- 25 files changed, 798 insertions(+), 68 deletions(-) create mode 100644 integrations/api_packages_maven_test.go create mode 100644 modules/packages/maven/metadata.go create mode 100644 modules/packages/maven/metadata_test.go create mode 100644 routers/api/v1/packages/maven/api.go create mode 100644 routers/api/v1/packages/maven/maven.go create mode 100644 routers/api/v1/packages/maven/package.go diff --git a/integrations/api_packages_generic_test.go b/integrations/api_packages_generic_test.go index 89e099b3fd365..aec78eb3c8fbb 100644 --- a/integrations/api_packages_generic_test.go +++ b/integrations/api_packages_generic_test.go @@ -33,7 +33,7 @@ func TestPackageGeneric(t *testing.T) { req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)) MakeRequest(t, req, http.StatusCreated) - ps, err := models.GetPackagesByRepositoryID(repository.ID) + ps, err := models.GetPackagesByRepositoryAndType(repository.ID, models.PackageGeneric) assert.NoError(t, err) assert.Len(t, ps, 1) assert.Equal(t, packageName, ps[0].Name) @@ -62,7 +62,7 @@ func TestPackageGeneric(t *testing.T) { req := NewRequest(t, "DELETE", url) MakeRequest(t, req, http.StatusOK) - ps, err := models.GetPackagesByRepositoryID(repository.ID) + ps, err := models.GetPackagesByRepositoryAndType(repository.ID, models.PackageGeneric) assert.NoError(t, err) assert.Empty(t, ps) }) diff --git a/integrations/api_packages_maven_test.go b/integrations/api_packages_maven_test.go new file mode 100644 index 0000000000000..d01e9234200c5 --- /dev/null +++ b/integrations/api_packages_maven_test.go @@ -0,0 +1,106 @@ +// Copyright 2017 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 ( + "fmt" + "net/http" + "strings" + "testing" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/packages/maven" + + jsoniter "github.com/json-iterator/go" + "github.com/stretchr/testify/assert" +) + +func TestPackageMaven(t *testing.T) { + defer prepareTestEnv(t)() + repository := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 2}).(*models.Repository) + user := models.AssertExistsAndLoadBean(t, &models.User{ID: repository.OwnerID}).(*models.User) + + groupID := "com.gitea" + artifactID := "test-project" + packageName := groupID + "-" + artifactID + packageVersion := "1.0.1" + packageDescription := "Test Description" + + root := fmt.Sprintf("/api/v1/repos/%s/%s/packages/maven/%s/%s", user.Name, repository.Name, strings.ReplaceAll(groupID, ".", "/"), artifactID) + filename := fmt.Sprintf("%s-%s.jar", packageName, packageVersion) + + putFile := func(t *testing.T, path, content string, expectedStatus int) { + req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, expectedStatus) + } + + t.Run("Upload", func(t *testing.T) { + putFile(t, fmt.Sprintf("/%s/%s", packageVersion, filename), "test", http.StatusCreated) + putFile(t, "/maven-metadata.xml", "test", http.StatusOK) + + ps, err := models.GetPackagesByRepositoryAndType(repository.ID, models.PackageMaven) + assert.NoError(t, err) + assert.Len(t, ps, 1) + assert.Equal(t, packageName, ps[0].Name) + assert.Equal(t, packageVersion, ps[0].Version) + + pfs, err := ps[0].GetFiles() + assert.NoError(t, err) + assert.Len(t, pfs, 1) + assert.Equal(t, filename, pfs[0].Name) + assert.Equal(t, int64(4), pfs[0].Size) + }) + + t.Run("UploadExists", func(t *testing.T) { + putFile(t, fmt.Sprintf("/%s/%s", packageVersion, filename), "test", http.StatusBadRequest) + }) + + t.Run("Download", func(t *testing.T) { + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, []byte("test"), resp.Body.Bytes()) + }) + + t.Run("UploadVerifySHA1", func(t *testing.T) { + t.Run("Missmatch", func(t *testing.T) { + putFile(t, fmt.Sprintf("/%s/%s.sha1", packageVersion, filename), "test", http.StatusBadRequest) + }) + t.Run("Valid", func(t *testing.T) { + putFile(t, fmt.Sprintf("/%s/%s.sha1", packageVersion, filename), "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", http.StatusOK) + }) + }) + + t.Run("UploadPOM", func(t *testing.T) { + pomContent := ` + + ` + groupID + ` + ` + artifactID + ` + ` + packageVersion + ` + ` + packageDescription + ` +` + + ps, err := models.GetPackagesByRepositoryAndType(repository.ID, models.PackageMaven) + assert.NoError(t, err) + assert.Len(t, ps, 1) + + var m *maven.Metadata + err = jsoniter.Unmarshal([]byte(ps[0].MetadataRaw), &m) + assert.NoError(t, err) + assert.Empty(t, m.Description) + + putFile(t, fmt.Sprintf("/%s/%s.pom", packageVersion, filename), pomContent, http.StatusCreated) + + ps, err = models.GetPackagesByRepositoryAndType(repository.ID, models.PackageMaven) + assert.NoError(t, err) + assert.Len(t, ps, 1) + + err = jsoniter.Unmarshal([]byte(ps[0].MetadataRaw), &m) + assert.NoError(t, err) + assert.Equal(t, packageDescription, m.Description) + }) +} diff --git a/integrations/api_packages_npm_test.go b/integrations/api_packages_npm_test.go index a8097c90e4aa9..d49e0e6900c7a 100644 --- a/integrations/api_packages_npm_test.go +++ b/integrations/api_packages_npm_test.go @@ -63,7 +63,7 @@ func TestPackageNPM(t *testing.T) { req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusCreated) - ps, err := models.GetPackagesByRepositoryID(repository.ID) + ps, err := models.GetPackagesByRepositoryAndType(repository.ID, models.PackageNPM) assert.NoError(t, err) assert.Len(t, ps, 1) assert.Equal(t, packageName, ps[0].Name) diff --git a/integrations/api_packages_nuget_test.go b/integrations/api_packages_nuget_test.go index a31d87f96f0da..4d2de4ba0b121 100644 --- a/integrations/api_packages_nuget_test.go +++ b/integrations/api_packages_nuget_test.go @@ -84,7 +84,7 @@ func TestPackageNuGet(t *testing.T) { req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusCreated) - ps, err := models.GetPackagesByRepositoryID(repository.ID) + ps, err := models.GetPackagesByRepositoryAndType(repository.ID, models.PackageNuGet) assert.NoError(t, err) assert.Len(t, ps, 1) assert.Equal(t, packageName, ps[0].Name) @@ -199,7 +199,7 @@ func TestPackageNuGet(t *testing.T) { req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusOK) - ps, err := models.GetPackagesByRepositoryID(repository.ID) + ps, err := models.GetPackagesByRepositoryAndType(repository.ID, models.PackageNuGet) assert.NoError(t, err) assert.Empty(t, ps) }) diff --git a/models/migrations/v188.go b/models/migrations/v188.go index 48c2deae06414..56ee43c6942f5 100644 --- a/models/migrations/v188.go +++ b/models/migrations/v188.go @@ -35,6 +35,7 @@ func addPackageTables(x *xorm.Engine) error { Size int64 Name string LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + HashMD5 string `xorm:"hash_md5"` HashSHA1 string `xorm:"hash_sha1"` HashSHA256 string `xorm:"hash_sha256"` HashSHA512 string `xorm:"hash_sha512"` diff --git a/models/package.go b/models/package.go index 15f13bcb81cbe..432602a455284 100644 --- a/models/package.go +++ b/models/package.go @@ -21,6 +21,7 @@ const ( PackageGeneric PackageType = iota PackageNuGet // 1 PackageNPM // 2 + PackageMaven // 3 ) var ( @@ -28,6 +29,10 @@ var ( ErrDuplicatePackage = errors.New("Package does exist already") // ErrPackageNotExist indicates a package not exist error ErrPackageNotExist = errors.New("Package does not exist") + // ErrDuplicatePackageFile indicates a duplicated package file error + ErrDuplicatePackageFile = errors.New("Package file does exist already") + // ErrPackageFileNotExist indicates a package file not exist error + ErrPackageFileNotExist = errors.New("Package file does not exist") ) // Package represents a package @@ -52,6 +57,7 @@ type PackageFile struct { Size int64 Name string LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + HashMD5 string `xorm:"hash_md5"` HashSHA1 string `xorm:"hash_sha1"` HashSHA256 string `xorm:"hash_sha256"` HashSHA512 string `xorm:"hash_sha512"` @@ -66,29 +72,55 @@ func (p *Package) GetFiles() ([]*PackageFile, error) { return packageFiles, x.Where("package_id = ?", p.ID).Find(&packageFiles) } +// GetFileByName gets the specific package file by name +func (p *Package) GetFileByName(name string) (*PackageFile, error) { + pf := &PackageFile{ + PackageID: p.ID, + LowerName: strings.ToLower(name), + } + has, err := x.Get(pf) + if err != nil { + return nil, err + } + if !has { + return nil, ErrDuplicatePackage + } + return pf, nil +} + // TryInsertPackage inserts a package // If a package already exists ErrDuplicatePackage is returned -func TryInsertPackage(p *Package) error { +func TryInsertPackage(p *Package) (*Package, error) { sess := x.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { - return err + return nil, err } - has, err := sess.Get(p) - if err != nil { - return err + key := &Package{ + RepoID: p.RepoID, + Type: p.Type, + LowerName: p.LowerName, + Version: p.Version, } + has, err := sess.Get(key) + if err != nil { + return nil, err + } if has { - return ErrDuplicatePackage + return key, ErrDuplicatePackage } - if _, err = sess.Insert(p); err != nil { - return err + return nil, err } + return p, sess.Commit() +} - return sess.Commit() +// UpdatePackage updates a package +func UpdatePackage(p *Package) error { + _, err := x.ID(p.ID).Update(p) + return err } // DeletePackageByID deletes a package and its files by ID @@ -103,7 +135,7 @@ func DeletePackageByID(packageID int64) error { // DeletePackagesByRepositoryID deletes all packages of a repository func DeletePackagesByRepositoryID(repositoryID int64) error { - packages, err := GetPackagesByRepositoryID(repositoryID) + packages, err := GetPackagesByRepository(repositoryID) if err != nil { return err } @@ -117,12 +149,18 @@ func DeletePackagesByRepositoryID(repositoryID int64) error { return nil } -// GetPackagesByRepositoryID returns all packages of a repository -func GetPackagesByRepositoryID(repositoryID int64) ([]*Package, error) { +// GetPackagesByRepository returns all packages of a repository +func GetPackagesByRepository(repositoryID int64) ([]*Package, error) { packages := make([]*Package, 0, 10) return packages, x.Where("repo_id = ?", repositoryID).Find(&packages) } +// GetPackagesByRepositoryAndType returns all packages of a repository with the specific type +func GetPackagesByRepositoryAndType(repositoryID int64, packageType PackageType) ([]*Package, error) { + packages := make([]*Package, 0, 10) + return packages, x.Where("repo_id = ? AND type = ?", repositoryID, packageType).Find(&packages) +} + // GetPackagesByName gets all repository packages with the specific name func GetPackagesByName(repositoryID int64, packageType PackageType, packageName string) ([]*Package, error) { packages := make([]*Package, 0, 10) @@ -171,10 +209,30 @@ func SearchPackages(repositoryID int64, packageType PackageType, query string, s return count, packages, err } -// InsertPackageFile inserts a package file -func InsertPackageFile(pf *PackageFile) error { - _, err := x.Insert(pf) - return err +// TryInsertPackageFile inserts a package file +func TryInsertPackageFile(pf *PackageFile) (*PackageFile, error) { + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return nil, err + } + + key := &PackageFile{ + PackageID: pf.PackageID, + LowerName: pf.LowerName, + } + + has, err := sess.Get(key) + if err != nil { + return nil, err + } + if has { + return key, ErrDuplicatePackageFile + } + if _, err = sess.Insert(pf); err != nil { + return nil, err + } + return pf, sess.Commit() } // UpdatePackageFile updates a package file diff --git a/modules/context/context.go b/modules/context/context.go index e575e90e10158..211ac14ec1c0b 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -326,23 +326,25 @@ func (ctx *Context) QueryOptionalBool(key string, defaults ...util.OptionalBool) } // UploadStream returns the request body or the first form file -func (ctx *Context) UploadStream() (io.ReadCloser, error) { +// Only form files need to get closed. +func (ctx *Context) UploadStream() (io.ReadCloser, bool, error) { contentType := strings.ToLower(ctx.Req.Header.Get("Content-Type")) if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || strings.HasPrefix(contentType, "multipart/form-data") { if err := ctx.Req.ParseMultipartForm(32 * 1024 * 1024); err != nil { - return nil, err + return nil, false, err } if ctx.Req.MultipartForm.File == nil { - return nil, http.ErrMissingFile + return nil, false, http.ErrMissingFile } for _, files := range ctx.Req.MultipartForm.File { if len(files) > 0 { - return files[0].Open() + r, err := files[0].Open() + return r, true, err } } - return nil, http.ErrMissingFile + return nil, false, http.ErrMissingFile } - return ctx.Req.Body, nil + return ctx.Req.Body, false, nil } // HandleText handles HTTP status code diff --git a/modules/packages/maven/metadata.go b/modules/packages/maven/metadata.go new file mode 100644 index 0000000000000..09df4c4e75804 --- /dev/null +++ b/modules/packages/maven/metadata.go @@ -0,0 +1,83 @@ +// Copyright 2021 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 maven + +import ( + "encoding/xml" + "io" +) + +// Metadata represents the metadata of a Maven package +type Metadata struct { + GroupID string `json:"group_id"` + ArtifactID string `json:"artifact_id"` + Name string `json:"name"` + Description string `json:"description"` + ProjectURL string `json:"project_url"` + Licenses []string `json:"licenses"` + Dependencies []*Dependency `json:"dependencies"` +} + +// Dependency represents a dependency of a Maven package +type Dependency struct { + GroupID string `json:"group_id"` + ArtifactID string `json:"artifact_id"` + Version string `json:"version"` +} + +type pomStruct struct { + XMLName xml.Name `xml:"project"` + GroupID string `xml:"groupId"` + ArtifactID string `xml:"artifactId"` + Version string `xml:"version"` + Name string `xml:"name"` + Description string `xml:"description"` + URL string `xml:"url"` + Licenses []struct { + Name string `xml:"name"` + URL string `xml:"url"` + Distribution string `xml:"distribution"` + } `xml:"licenses>license"` + Dependencies []struct { + GroupID string `xml:"groupId"` + ArtifactID string `xml:"artifactId"` + Version string `xml:"version"` + Scope string `xml:"scope"` + } `xml:"dependencies>dependency"` +} + +// ParsePackageMetaData parses the metadata of a pom file +func ParsePackageMetaData(r io.Reader) (*Metadata, error) { + var pom pomStruct + if err := xml.NewDecoder(r).Decode(&pom); err != nil { + return nil, err + } + + licenses := make([]string, 0, len(pom.Licenses)) + for _, l := range pom.Licenses { + if l.Name != "" { + licenses = append(licenses, l.Name) + } + } + + dependencies := make([]*Dependency, 0, len(pom.Dependencies)) + for _, d := range pom.Dependencies { + dependencies = append(dependencies, &Dependency{ + GroupID: d.GroupID, + ArtifactID: d.ArtifactID, + Version: d.Version, + }) + } + + return &Metadata{ + GroupID: pom.GroupID, + ArtifactID: pom.ArtifactID, + Name: pom.Name, + Description: pom.Description, + ProjectURL: pom.URL, + Licenses: licenses, + Dependencies: dependencies, + }, nil +} diff --git a/modules/packages/maven/metadata_test.go b/modules/packages/maven/metadata_test.go new file mode 100644 index 0000000000000..a17d4565603a0 --- /dev/null +++ b/modules/packages/maven/metadata_test.go @@ -0,0 +1,73 @@ +// Copyright 2021 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 maven + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + groupID = "org.gitea" + artifactID = "my-project" + version = "1.0.1" + name = "My Gitea Project" + description = "Package Description" + projectURL = "https://gitea.io" + license = "MIT" + dependencyGroupID = "org.gitea.core" + dependencyArtifactID = "git" + dependencyVersion = "5.0.0" +) + +const pomContent = ` + + ` + groupID + ` + ` + artifactID + ` + ` + version + ` + ` + name + ` + ` + description + ` + ` + projectURL + ` + + + ` + license + ` + + + + + ` + dependencyGroupID + ` + ` + dependencyArtifactID + ` + ` + dependencyVersion + ` + + +` + +func TestParsePackageMetaData(t *testing.T) { + t.Run("InvalidFile", func(t *testing.T) { + m, err := ParsePackageMetaData(strings.NewReader("")) + assert.Nil(t, m) + assert.Error(t, err) + }) + + t.Run("Valid", func(t *testing.T) { + m, err := ParsePackageMetaData(strings.NewReader(pomContent)) + assert.NoError(t, err) + assert.NotNil(t, m) + + assert.Equal(t, groupID, m.GroupID) + assert.Equal(t, artifactID, m.ArtifactID) + assert.Equal(t, name, m.Name) + assert.Equal(t, description, m.Description) + assert.Equal(t, projectURL, m.ProjectURL) + assert.Len(t, m.Licenses, 1) + assert.Equal(t, license, m.Licenses[0]) + assert.Len(t, m.Dependencies, 1) + assert.Equal(t, dependencyGroupID, m.Dependencies[0].GroupID) + assert.Equal(t, dependencyArtifactID, m.Dependencies[0].ArtifactID) + assert.Equal(t, dependencyVersion, m.Dependencies[0].Version) + }) +} diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index b0766f7a92e15..8eff6dac1140e 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -141,7 +141,7 @@ func ParsePackage(r io.Reader) (*Package, error) { Description: meta.Description, Author: meta.Author.Name, License: meta.License, - Homepage: meta.Homepage, + ProjectURL: meta.Homepage, Dependencies: meta.Dependencies, Readme: meta.Readme, }, diff --git a/modules/packages/npm/creator_test.go b/modules/packages/npm/creator_test.go index c777bbac91d36..aba8fa9c0057f 100644 --- a/modules/packages/npm/creator_test.go +++ b/modules/packages/npm/creator_test.go @@ -217,7 +217,7 @@ func TestParsePackage(t *testing.T) { assert.Equal(t, packageDescription, p.Metadata.Readme) assert.Equal(t, packageAuthor, p.Metadata.Author) assert.Equal(t, "MIT", p.Metadata.License) - assert.Equal(t, "https://gitea.io/", p.Metadata.Homepage) + assert.Equal(t, "https://gitea.io/", p.Metadata.ProjectURL) assert.Contains(t, p.Metadata.Dependencies, "package") assert.Equal(t, "1.2.0", p.Metadata.Dependencies["package"]) }) diff --git a/modules/packages/npm/metadata.go b/modules/packages/npm/metadata.go index f580c868e9e6d..a2e40b5b6c9cb 100644 --- a/modules/packages/npm/metadata.go +++ b/modules/packages/npm/metadata.go @@ -9,7 +9,7 @@ type Metadata struct { Description string `json:"description"` Author string `json:"author"` License string `json:"license"` - Homepage string `json:"homepage"` + ProjectURL string `json:"project_url"` Dependencies map[string]string `json:"dependencies"` Readme string `json:"readme"` } diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index f67ac78c4ed6c..ac6203f049b0d 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -106,9 +106,7 @@ func ParsePackageMetaData(r io.ReaderAt, size int64) (*Metadata, error) { // ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package func ParseNuspecMetaData(r io.Reader) (*Metadata, error) { var p nuspecPackage - dec := xml.NewDecoder(r) - err := dec.Decode(&p) - if err != nil { + if err := xml.NewDecoder(r).Decode(&p); err != nil { return nil, err } diff --git a/modules/packages/nuget/metadata_test.go b/modules/packages/nuget/metadata_test.go index b72b282bab8c3..6e50256dbcf41 100644 --- a/modules/packages/nuget/metadata_test.go +++ b/modules/packages/nuget/metadata_test.go @@ -17,11 +17,11 @@ const ( id = "System.Gitea" semver = "1.0.1" authors = "Gitea Authors" - projectURL = "https://gitea.com" + projectURL = "https://gitea.io" description = "Package Description" summary = "Package Summary" releaseNotes = "Package Release Notes" - repositoryURL = "https://gitea.com/gitea/gitea" + repositoryURL = "https://gitea.io/gitea/gitea" targetFramework = ".NETStandard2.1" dependencyID = "System.Text.Json" dependencyVersion = "5.0.0" diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 1b8907b90be28..195e9756143ac 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -80,6 +80,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/notify" "code.gitea.io/gitea/routers/api/v1/org" "code.gitea.io/gitea/routers/api/v1/packages/generic" + "code.gitea.io/gitea/routers/api/v1/packages/maven" "code.gitea.io/gitea/routers/api/v1/packages/npm" "code.gitea.io/gitea/routers/api/v1/packages/nuget" "code.gitea.io/gitea/routers/api/v1/repo" @@ -982,6 +983,10 @@ func Routes() *web.Route { m.Delete("" /*reqRepoWriter(models.UnitTypePackage),*/, generic.DeletePackage) }) }, reqToken()) + m.Group("/maven", func() { + m.Put("/*" /*reqRepoWriter(models.UnitTypePackage),*/, maven.UploadPackageFile) + m.Get("/*" /*reqRepoWriter(models.UnitTypePackage),*/, maven.DownloadPackageFile) + }, reqToken()) m.Group("/nuget", func() { m.Put("/" /*reqRepoWriter(models.UnitTypePackage),*/, nuget.UploadPackage) m.Get("/index.json", nuget.ServiceIndex) diff --git a/routers/api/v1/packages/generic/generic.go b/routers/api/v1/packages/generic/generic.go index 5b65264ed1e3d..3ee1c6f0622db 100644 --- a/routers/api/v1/packages/generic/generic.go +++ b/routers/api/v1/packages/generic/generic.go @@ -32,7 +32,7 @@ func DownloadPackageContent(ctx *context.APIContext) { s, pf, err := package_service.GetPackageFileStream(ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion, filename) if err != nil { - if err == models.ErrPackageNotExist { + if err == models.ErrPackageNotExist || err == models.ErrPackageFileNotExist { ctx.Error(http.StatusNotFound, "", err) return } @@ -53,12 +53,14 @@ func UploadPackage(ctx *context.APIContext) { return } - upload, err := ctx.UploadStream() + upload, close, err := ctx.UploadStream() if err != nil { ctx.Error(http.StatusBadRequest, "", err) return } - defer upload.Close() + if close { + defer upload.Close() + } buf, err := filebuffer.CreateFromReader(upload, 32*1024*1024) if err != nil { diff --git a/routers/api/v1/packages/maven/api.go b/routers/api/v1/packages/maven/api.go new file mode 100644 index 0000000000000..e00c08ceeb8a0 --- /dev/null +++ b/routers/api/v1/packages/maven/api.go @@ -0,0 +1,36 @@ +// Copyright 2021 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 maven + +import ( + "encoding/xml" +) + +// MetadataResponse https://maven.apache.org/ref/3.2.5/maven-repository-metadata/repository-metadata.html +type MetadataResponse struct { + XMLName xml.Name `xml:"metadata"` + GroupID string `xml:"groupId"` + ArtifactID string `xml:"artifactId"` + Latest string `xml:"versioning>latest"` + Version []string `xml:"versioning>versions>version"` +} + +func createMetadataResponse(packages []*Package) *MetadataResponse { + sortedPackages := sortPackagesByVersionASC(packages) + + versions := make([]string, 0, len(sortedPackages)) + for _, p := range sortedPackages { + versions = append(versions, p.Version) + } + + latest := sortedPackages[len(sortedPackages)-1] + + return &MetadataResponse{ + GroupID: latest.Metadata.GroupID, + ArtifactID: latest.Metadata.ArtifactID, + Latest: latest.Version, + Version: versions, + } +} diff --git a/routers/api/v1/packages/maven/maven.go b/routers/api/v1/packages/maven/maven.go new file mode 100644 index 0000000000000..8b46f929ba83c --- /dev/null +++ b/routers/api/v1/packages/maven/maven.go @@ -0,0 +1,295 @@ +// Copyright 2021 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 maven + +import ( + "crypto/md5" + "crypto/sha1" + "encoding/xml" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "path/filepath" + "regexp" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/packages" + maven_module "code.gitea.io/gitea/modules/packages/maven" + "code.gitea.io/gitea/modules/util/filebuffer" + + package_service "code.gitea.io/gitea/services/packages" + + jsoniter "github.com/json-iterator/go" +) + +const mavenMetadataFile = "maven-metadata.xml" + +var ( + errInvalidParameters = errors.New("Request parameters are invalid") + illegalCharacters = regexp.MustCompile(`[\\/:"<>|?\*]`) +) + +// DownloadPackageFile serves the content of a package +func DownloadPackageFile(ctx *context.APIContext) { + params, err := extractPathParameters(ctx) + if err != nil { + ctx.Error(http.StatusBadRequest, "", err) + return + } + + if params.IsMeta { + serveMavenMetadata(ctx, params) + } else { + servePackageFile(ctx, params) + } +} + +func serveMavenMetadata(ctx *context.APIContext, params parameters) { + if params.Version == "" { + // /com/foo/project/maven-metadata.xml[.sha1/.md5] + + packageName := params.GroupID + "-" + params.ArtifactID + packages, err := models.GetPackagesByName(ctx.Repo.Repository.ID, models.PackageMaven, packageName) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + if len(packages) == 0 { + ctx.Error(http.StatusNotFound, "", err) + return + } + + mavenPackages, err := intializePackages(packages) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + xmlMetadata, err := xml.Marshal(createMetadataResponse(mavenPackages)) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + xmlMetadataWithHeader := append([]byte(xml.Header), xmlMetadata...) + + switch strings.ToLower(filepath.Ext(params.Filename)) { + case ".sha1": + ctx.PlainText(http.StatusOK, []byte(fmt.Sprintf("%x", sha1.Sum(xmlMetadataWithHeader)))) + case ".md5": + ctx.PlainText(http.StatusOK, []byte(fmt.Sprintf("%x", md5.Sum(xmlMetadataWithHeader)))) + default: + ctx.PlainText(http.StatusOK, xmlMetadataWithHeader) + } + } else { + // /com/foo/project/1-SNAPSHOT/maven-metadata.xml[.sha1/.md5] + + ctx.Error(http.StatusNotFound, "", "") + } +} + +func servePackageFile(ctx *context.APIContext, params parameters) { + packageName := params.GroupID + "-" + params.ArtifactID + + p, err := models.GetPackageByNameAndVersion(ctx.Repo.Repository.ID, models.PackageMaven, packageName, params.Version) + if err == models.ErrPackageNotExist { + ctx.Error(http.StatusNotFound, "", err) + return + } + + filename := params.Filename + + ext := strings.ToLower(filepath.Ext(filename)) + if ext == ".sha1" || ext == ".md5" { + filename = filename[:len(filename)-len(ext)] + } + + pf, err := p.GetFileByName(filename) + if err != nil { + if err == models.ErrPackageFileNotExist { + ctx.Error(http.StatusNotFound, "", "") + return + } + log.Error("Error getting file by name: %v", err) + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + if ext == ".sha1" { + ctx.PlainText(http.StatusOK, []byte(pf.HashSHA1)) + return + } + if ext == ".md5" { + ctx.PlainText(http.StatusOK, []byte(pf.HashMD5)) + return + } + s, err := packages.NewContentStore().Get(p.ID, pf.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + } + defer s.Close() + + ctx.ServeStream(s, pf.Name) +} + +// UploadPackageFile adds a file to the package. If the package does not exist, it gets created. +func UploadPackageFile(ctx *context.APIContext) { + params, err := extractPathParameters(ctx) + if err != nil { + ctx.Error(http.StatusBadRequest, "", err) + return + } + + log.Trace("Parameters: %+v", params) + + if params.IsMeta { + ctx.PlainText(http.StatusOK, nil) + return + } + + packageName := params.GroupID + "-" + params.ArtifactID + + p, err := package_service.CreatePackage( + ctx.User, + ctx.Repo.Repository, + models.PackageMaven, + packageName, + params.Version, + &maven_module.Metadata{ + GroupID: params.GroupID, + ArtifactID: params.ArtifactID, + }, + true, + ) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + buf, err := filebuffer.CreateFromReader(ctx.Req.Body, 32*1024*1024) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + defer buf.Close() + + ext := filepath.Ext(params.Filename) + + if ext == ".sha1" || ext == ".md5" { + if ext == ".sha1" { + pf, err := p.GetFileByName(params.Filename[:len(params.Filename)-5]) + if err != nil { + if err == models.ErrPackageFileNotExist { + ctx.Error(http.StatusNotFound, "", "") + return + } + log.Error("GetFileByName: %v", err) + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + hash, err := ioutil.ReadAll(buf) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + if pf.HashSHA1 != string(hash) { + ctx.Error(http.StatusBadRequest, "", "hash mismatch") + return + } + } + + ctx.PlainText(http.StatusOK, nil) + return + } + + // If it's the package pom file extract the metadata + if ext == ".pom" { + metadata, err := maven_module.ParsePackageMetaData(buf) + if err != nil { + log.Error("Error parsing package metadata: %v", err) + } + if metadata != nil { + raw, err := jsoniter.Marshal(metadata) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + p.MetadataRaw = string(raw) + if err := models.UpdatePackage(p); err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + } + if _, err := buf.Seek(0, io.SeekStart); err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + } + + _, err = package_service.AddFileToPackage(p, params.Filename, buf.Size(), buf) + if err != nil { + if err == models.ErrDuplicatePackageFile { + ctx.Error(http.StatusBadRequest, "", err) + return + } + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + ctx.PlainText(http.StatusCreated, nil) +} + +type parameters struct { + GroupID string + ArtifactID string + Version string + Filename string + IsMeta bool +} + +func extractPathParameters(ctx *context.APIContext) (parameters, error) { + parts := strings.Split(ctx.Params("*"), "/") + + p := parameters{ + Filename: parts[len(parts)-1], + } + + p.IsMeta = p.Filename == mavenMetadataFile || p.Filename == mavenMetadataFile+".sha1" || p.Filename == mavenMetadataFile+".md5" + + parts = parts[:len(parts)-1] + if len(parts) == 0 { + return p, errInvalidParameters + } + + p.Version = parts[len(parts)-1] + if p.IsMeta && !strings.HasSuffix(p.Version, "-SNAPSHOT") { + p.Version = "" + } else { + parts = parts[:len(parts)-1] + } + + if illegalCharacters.MatchString(p.Version) { + return p, errInvalidParameters + } + + if len(parts) < 2 { + return p, errInvalidParameters + } + + p.ArtifactID = parts[len(parts)-1] + p.GroupID = strings.Join(parts[:len(parts)-1], ".") + + if illegalCharacters.MatchString(p.GroupID) || illegalCharacters.MatchString(p.ArtifactID) { + return p, errInvalidParameters + } + + return p, nil +} diff --git a/routers/api/v1/packages/maven/package.go b/routers/api/v1/packages/maven/package.go new file mode 100644 index 0000000000000..9ef5d0550d622 --- /dev/null +++ b/routers/api/v1/packages/maven/package.go @@ -0,0 +1,59 @@ +// Copyright 2021 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 maven + +import ( + "sort" + + "code.gitea.io/gitea/models" + maven_module "code.gitea.io/gitea/modules/packages/maven" + + jsoniter "github.com/json-iterator/go" +) + +// Package represents a package with Maven metadata +type Package struct { + *models.Package + Metadata *maven_module.Metadata +} + +func intializePackages(packages []*models.Package) ([]*Package, error) { + pgs := make([]*Package, 0, len(packages)) + for _, p := range packages { + np, err := intializePackage(p) + if err != nil { + return nil, err + } + pgs = append(pgs, np) + } + return pgs, nil +} + +func intializePackage(p *models.Package) (*Package, error) { + var m *maven_module.Metadata + err := jsoniter.Unmarshal([]byte(p.MetadataRaw), &m) + if err != nil { + return nil, err + } + if m == nil { + m = &maven_module.Metadata{} + } + + return &Package{ + Package: p, + Metadata: m, + }, nil +} + +func sortPackagesByVersionASC(packages []*Package) []*Package { + sortedPackages := make([]*Package, len(packages)) + copy(sortedPackages, packages) + + sort.Slice(sortedPackages, func(i, j int) bool { + return sortedPackages[i].Version < sortedPackages[j].Version + }) + + return sortedPackages +} diff --git a/routers/api/v1/packages/npm/api.go b/routers/api/v1/packages/npm/api.go index a916d97747519..d4e9592b48510 100644 --- a/routers/api/v1/packages/npm/api.go +++ b/routers/api/v1/packages/npm/api.go @@ -32,7 +32,7 @@ func createPackageMetadataResponse(registryURL string, packages []*Package) *npm DistTags: distTags, Description: latest.Metadata.Description, Readme: latest.Metadata.Readme, - Homepage: latest.Metadata.Homepage, + Homepage: latest.Metadata.ProjectURL, Author: npm_module.User{Name: latest.Metadata.Author}, License: latest.Metadata.License, Versions: versions, @@ -48,7 +48,7 @@ func createPackageMetadataVersion(registryURL string, p *Package) *npm_module.Pa Version: p.Package.Version, Description: p.Metadata.Description, Author: npm_module.User{Name: p.Metadata.Author}, - Homepage: p.Metadata.Homepage, + Homepage: p.Metadata.ProjectURL, License: p.Metadata.License, Dependencies: p.Metadata.Dependencies, Readme: p.Metadata.Readme, diff --git a/routers/api/v1/packages/npm/npm.go b/routers/api/v1/packages/npm/npm.go index 99caee7c31ff1..2d39bd01612b3 100644 --- a/routers/api/v1/packages/npm/npm.go +++ b/routers/api/v1/packages/npm/npm.go @@ -62,7 +62,7 @@ func DownloadPackageContent(ctx *context.APIContext) { s, pf, err := package_service.GetPackageFileStream(ctx.Repo.Repository, models.PackageNPM, packageName, packageVersion, filename) if err != nil { - if err == models.ErrPackageNotExist { + if err == models.ErrPackageNotExist || err == models.ErrPackageFileNotExist { ctx.Error(http.StatusNotFound, "", err) return } @@ -76,8 +76,6 @@ func DownloadPackageContent(ctx *context.APIContext) { // UploadPackage creates a new package func UploadPackage(ctx *context.APIContext) { - defer ctx.Req.Body.Close() - npmPackage, err := npm_module.ParsePackage(ctx.Req.Body) if err != nil { ctx.Error(http.StatusBadRequest, "", err) diff --git a/routers/api/v1/packages/npm/package.go b/routers/api/v1/packages/npm/package.go index aa845c58b5a81..b74f368cea256 100644 --- a/routers/api/v1/packages/npm/package.go +++ b/routers/api/v1/packages/npm/package.go @@ -23,15 +23,15 @@ type Package struct { } func intializePackages(packages []*models.Package) ([]*Package, error) { - nugetPackages := make([]*Package, 0, len(packages)) + pgs := make([]*Package, 0, len(packages)) for _, p := range packages { np, err := intializePackage(p) if err != nil { return nil, err } - nugetPackages = append(nugetPackages, np) + pgs = append(pgs, np) } - return nugetPackages, nil + return pgs, nil } func intializePackage(p *models.Package) (*Package, error) { diff --git a/routers/api/v1/packages/nuget/nuget.go b/routers/api/v1/packages/nuget/nuget.go index f3cde14ce5eb4..faaef9f15c1aa 100644 --- a/routers/api/v1/packages/nuget/nuget.go +++ b/routers/api/v1/packages/nuget/nuget.go @@ -6,6 +6,7 @@ package nuget import ( "fmt" + "io" "net/http" "strings" @@ -145,7 +146,7 @@ func DownloadPackageContent(ctx *context.APIContext) { s, pf, err := package_service.GetPackageFileStream(ctx.Repo.Repository, models.PackageNuGet, packageName, packageVersion, filename) if err != nil { - if err == models.ErrPackageNotExist { + if err == models.ErrPackageNotExist || err == models.ErrPackageFileNotExist { ctx.Error(http.StatusNotFound, "", err) return } @@ -160,12 +161,14 @@ func DownloadPackageContent(ctx *context.APIContext) { // UploadPackage creates a new package with the metadata contained in the uploaded nupgk file // https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#push-a-package func UploadPackage(ctx *context.APIContext) { - upload, err := ctx.UploadStream() + upload, close, err := ctx.UploadStream() if err != nil { ctx.Error(http.StatusBadRequest, "", err) return } - defer upload.Close() + if close { + defer upload.Close() + } buf, err := filebuffer.CreateFromReader(upload, 32*1024*1024) if err != nil { @@ -179,6 +182,10 @@ func UploadPackage(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "", err) return } + if _, err := buf.Seek(0, io.SeekStart); err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } p, err := package_service.CreatePackage( ctx.User, diff --git a/routers/api/v1/packages/nuget/package.go b/routers/api/v1/packages/nuget/package.go index 442b134321c18..c4be91e77aa3b 100644 --- a/routers/api/v1/packages/nuget/package.go +++ b/routers/api/v1/packages/nuget/package.go @@ -22,15 +22,15 @@ type Package struct { } func intializePackages(packages []*models.Package) ([]*Package, error) { - nugetPackages := make([]*Package, 0, len(packages)) + pgs := make([]*Package, 0, len(packages)) for _, p := range packages { np, err := intializePackage(p) if err != nil { return nil, err } - nugetPackages = append(nugetPackages, np) + pgs = append(pgs, np) } - return nugetPackages, nil + return pgs, nil } func intializePackage(p *models.Package) (*Package, error) { diff --git a/services/packages/packages.go b/services/packages/packages.go index b75b2542425e1..b8b88fd278ef6 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -5,6 +5,7 @@ package packages import ( + "crypto/md5" "crypto/sha1" "crypto/sha256" "crypto/sha512" @@ -27,6 +28,8 @@ func CreatePackage(creator *models.User, repository *models.Repository, packageT return nil, err } + log.Trace("Creating package: %v, %v, %v, %s, %s, %+v, %v", creator.ID, repository.ID, packageType, name, version, metadata, allowDuplicate) + p := &models.Package{ RepoID: repository.ID, CreatorID: creator.ID, @@ -36,7 +39,7 @@ func CreatePackage(creator *models.User, repository *models.Repository, packageT Version: version, MetadataRaw: string(metadataJSON), } - if err := models.TryInsertPackage(p); err != nil { + if p, err = models.TryInsertPackage(p); err != nil { if err == models.ErrDuplicatePackage { if allowDuplicate { return p, nil @@ -51,32 +54,40 @@ func CreatePackage(creator *models.User, repository *models.Repository, packageT // AddFileToPackage adds a new file to package and stores its content func AddFileToPackage(p *models.Package, filename string, size int64, r io.Reader) (*models.PackageFile, error) { + log.Trace("Creating package file: %v, %v, %s", p.ID, size, filename) + pf := &models.PackageFile{ PackageID: p.ID, Size: size, Name: filename, LowerName: strings.ToLower(filename), } - if err := models.InsertPackageFile(pf); err != nil { + var err error + if pf, err = models.TryInsertPackageFile(pf); err != nil { + if err == models.ErrDuplicatePackageFile { + return nil, err + } log.Error("Error inserting package file: %v", err) return nil, err } + md5 := md5.New() h1 := sha1.New() h256 := sha256.New() h512 := sha512.New() - r = io.TeeReader(r, io.MultiWriter(h1, h256, h512)) + r = io.TeeReader(r, io.MultiWriter(md5, h1, h256, h512)) contentStore := packages_module.NewContentStore() - err := func() error { + err = func() error { err := contentStore.Save(p.ID, pf.ID, r, size) if err != nil { log.Error("Error saving package file in content store: %v", err) return err } + pf.HashMD5 = fmt.Sprintf("%x", md5.Sum(nil)) pf.HashSHA1 = fmt.Sprintf("%x", h1.Sum(nil)) pf.HashSHA256 = fmt.Sprintf("%x", h256.Sum(nil)) pf.HashSHA512 = fmt.Sprintf("%x", h512.Sum(nil)) @@ -99,6 +110,8 @@ func AddFileToPackage(p *models.Package, filename string, size int64, r io.Reade // DeletePackage deletes a package and all associated files func DeletePackage(repository *models.Repository, packageType models.PackageType, name, version string) error { + log.Trace("Deleting package: %v, %v, %s, %s", repository.ID, packageType, name, version) + p, err := models.GetPackageByNameAndVersion(repository.ID, packageType, name, version) if err != nil { if err == models.ErrPackageNotExist { @@ -117,7 +130,7 @@ func DeletePackage(repository *models.Repository, packageType models.PackageType contentStore := packages_module.NewContentStore() for _, pf := range pfs { if err := contentStore.Delete(p.ID, pf.ID); err != nil { - log.Error("Error deleting package file: %v", err) + log.Error("Error deleting package file [%s]: %v", pf.Name, err) return err } } @@ -132,24 +145,18 @@ func DeletePackage(repository *models.Repository, packageType models.PackageType // GetPackageFileStream returns the content of the specific package file func GetPackageFileStream(repository *models.Repository, packageType models.PackageType, name, version, filename string) (io.ReadCloser, *models.PackageFile, error) { + log.Trace("Getting package file stream: %v, %v, %s, %s, %s", repository.ID, packageType, name, version, filename) + p, err := models.GetPackageByNameAndVersion(repository.ID, packageType, name, version) if err != nil { return nil, nil, err } - pfs, err := p.GetFiles() + pf, err := p.GetFileByName(filename) if err != nil { return nil, nil, err } - filename = strings.ToLower(filename) - - for _, pf := range pfs { - if pf.LowerName == filename { - s, err := packages_module.NewContentStore().Get(p.ID, pf.ID) - return s, pf, err - } - } - - return nil, nil, models.ErrPackageNotExist + s, err := packages_module.NewContentStore().Get(p.ID, pf.ID) + return s, pf, err } From 8aaf412c346bbe4d2e31ac6ece00cd38687f9c42 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sun, 18 Jul 2021 14:35:06 +0000 Subject: [PATCH 009/130] Added PyPI package registry. --- integrations/api_packages_pypi_test.go | 144 +++++++++++++++++++++ models/package.go | 1 + modules/packages/pypi/metadata.go | 15 +++ routers/api/v1/api.go | 6 + routers/api/v1/packages/generic/generic.go | 3 + routers/api/v1/packages/npm/npm.go | 1 + routers/api/v1/packages/nuget/nuget.go | 1 + routers/api/v1/packages/pypi/package.go | 53 ++++++++ routers/api/v1/packages/pypi/pypi.go | 143 ++++++++++++++++++++ templates/api/packages/pypi/simple.tmpl | 15 +++ 10 files changed, 382 insertions(+) create mode 100644 integrations/api_packages_pypi_test.go create mode 100644 modules/packages/pypi/metadata.go create mode 100644 routers/api/v1/packages/pypi/package.go create mode 100644 routers/api/v1/packages/pypi/pypi.go create mode 100644 templates/api/packages/pypi/simple.tmpl diff --git a/integrations/api_packages_pypi_test.go b/integrations/api_packages_pypi_test.go new file mode 100644 index 0000000000000..5c3b9d4015bae --- /dev/null +++ b/integrations/api_packages_pypi_test.go @@ -0,0 +1,144 @@ +// Copyright 2017 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 ( + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "regexp" + "strings" + "testing" + + "code.gitea.io/gitea/models" + + "github.com/stretchr/testify/assert" +) + +func TestPackagePyPI(t *testing.T) { + defer prepareTestEnv(t)() + repository := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 2}).(*models.Repository) + user := models.AssertExistsAndLoadBean(t, &models.User{ID: repository.OwnerID}).(*models.User) + + packageName := "test-package" + packageVersion := "1.0.1" + packageAuthor := "KN4CK3R" + packageDescription := "Test Description" + + content := "test" + hashSHA256 := "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + + root := fmt.Sprintf("/api/v1/repos/%s/%s/packages/pypi", user.Name, repository.Name) + + uploadFile := func(t *testing.T, filename, content string, expectedStatus int) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, _ := writer.CreateFormFile("content", filename) + _, _ = io.Copy(part, strings.NewReader(content)) + + writer.WriteField("name", packageName) + writer.WriteField("version", packageVersion) + writer.WriteField("author", packageAuthor) + writer.WriteField("summary", packageDescription) + writer.WriteField("description", packageDescription) + writer.WriteField("sha256_digest", hashSHA256) + writer.WriteField("requires_python", "3.6") + + _ = writer.Close() + + req := NewRequestWithBody(t, "POST", root, body) + req.Header.Add("Content-Type", writer.FormDataContentType()) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, expectedStatus) + } + + t.Run("Upload", func(t *testing.T) { + filename := "test.whl" + uploadFile(t, filename, content, http.StatusCreated) + + ps, err := models.GetPackagesByRepositoryAndType(repository.ID, models.PackagePyPI) + assert.NoError(t, err) + assert.Len(t, ps, 1) + assert.Equal(t, packageName, ps[0].Name) + assert.Equal(t, packageVersion, ps[0].Version) + + pfs, err := ps[0].GetFiles() + assert.NoError(t, err) + assert.Len(t, pfs, 1) + assert.Equal(t, filename, pfs[0].Name) + assert.Equal(t, int64(4), pfs[0].Size) + }) + + t.Run("UploadAddFile", func(t *testing.T) { + filename := "test.tar.gz" + uploadFile(t, filename, content, http.StatusCreated) + + ps, err := models.GetPackagesByRepositoryAndType(repository.ID, models.PackagePyPI) + assert.NoError(t, err) + assert.Len(t, ps, 1) + assert.Equal(t, packageName, ps[0].Name) + assert.Equal(t, packageVersion, ps[0].Version) + + pf, err := ps[0].GetFileByName(filename) + assert.NoError(t, err) + assert.NotNil(t, pf) + assert.Equal(t, filename, pf.Name) + assert.Equal(t, int64(4), pf.Size) + + pfs, err := ps[0].GetFiles() + assert.NoError(t, err) + assert.Len(t, pfs, 2) + }) + + t.Run("UploadHashMismatch", func(t *testing.T) { + filename := "test2.whl" + uploadFile(t, filename, "dummy", http.StatusBadRequest) + }) + + t.Run("UploadExists", func(t *testing.T) { + uploadFile(t, "test.whl", content, http.StatusBadRequest) + uploadFile(t, "test.tar.gz", content, http.StatusBadRequest) + }) + + t.Run("Download", func(t *testing.T) { + downloadFile := func(filename string) { + req := NewRequest(t, "GET", fmt.Sprintf("%s/files/%s/%s/%s", root, packageName, packageVersion, filename)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, []byte(content), resp.Body.Bytes()) + } + + downloadFile("test.whl") + downloadFile("test.tar.gz") + }) + + t.Run("PackageMetadata", func(t *testing.T) { + req := NewRequest(t, "GET", fmt.Sprintf("%s/simple/%s", root, packageName)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + nodes := htmlDoc.doc.Find("a").Nodes + assert.Len(t, nodes, 2) + + hrefMatcher := regexp.MustCompile(fmt.Sprintf(`%s/files/%s/%s/test\..+#sha256-%s`, root, packageName, packageVersion, hashSHA256)) + + for _, a := range nodes { + for _, att := range a.Attr { + switch att.Key { + case "href": + assert.Regexp(t, hrefMatcher, att.Val) + case "data-requires-python": + assert.Equal(t, "3.6", att.Val) + default: + t.Fail() + } + } + } + }) +} diff --git a/models/package.go b/models/package.go index 432602a455284..13dcdbb401d68 100644 --- a/models/package.go +++ b/models/package.go @@ -22,6 +22,7 @@ const ( PackageNuGet // 1 PackageNPM // 2 PackageMaven // 3 + PackagePyPI // 4 ) var ( diff --git a/modules/packages/pypi/metadata.go b/modules/packages/pypi/metadata.go new file mode 100644 index 0000000000000..e3998af794789 --- /dev/null +++ b/modules/packages/pypi/metadata.go @@ -0,0 +1,15 @@ +// Copyright 2021 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 pypi + +// Metadata represents the metadata of a PyPI package +type Metadata struct { + Author string `json:"author"` + Description string `json:"description"` + Summary string `json:"summary"` + ProjectURL string `json:"project_url"` + License string `json:"license"` + RequiresPython string `json:"requires_python"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 195e9756143ac..6b69f14671cce 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -83,6 +83,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/packages/maven" "code.gitea.io/gitea/routers/api/v1/packages/npm" "code.gitea.io/gitea/routers/api/v1/packages/nuget" + "code.gitea.io/gitea/routers/api/v1/packages/pypi" "code.gitea.io/gitea/routers/api/v1/repo" "code.gitea.io/gitea/routers/api/v1/settings" _ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation @@ -1010,6 +1011,11 @@ func Routes() *web.Route { m.Get("/-/{version}/{filename}", npm.DownloadPackageContent) }) }, reqToken()) + m.Group("/pypi", func() { + m.Post("/" /*reqRepoWriter(models.UnitTypePackage),*/, pypi.UploadPackageFile) + m.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageContent) + m.Get("/simple/{id}", pypi.PackageMetadata) + }, reqBasicAuth()) }) }, repoAssignment()) }) diff --git a/routers/api/v1/packages/generic/generic.go b/routers/api/v1/packages/generic/generic.go index 3ee1c6f0622db..89da2a19f43c2 100644 --- a/routers/api/v1/packages/generic/generic.go +++ b/routers/api/v1/packages/generic/generic.go @@ -96,6 +96,7 @@ func UploadPackage(ctx *context.APIContext) { log.Error("Error deleting package by id: %v", err) } ctx.Error(http.StatusInternalServerError, "", "") + return } ctx.PlainText(http.StatusCreated, nil) @@ -116,7 +117,9 @@ func DeletePackage(ctx *context.APIContext) { return } ctx.Error(http.StatusInternalServerError, "", "") + return } + ctx.PlainText(http.StatusOK, nil) } func sanitizeParameters(ctx *context.APIContext) (string, string, string, error) { diff --git a/routers/api/v1/packages/npm/npm.go b/routers/api/v1/packages/npm/npm.go index 2d39bd01612b3..440f5445c4519 100644 --- a/routers/api/v1/packages/npm/npm.go +++ b/routers/api/v1/packages/npm/npm.go @@ -106,6 +106,7 @@ func UploadPackage(ctx *context.APIContext) { log.Error("Error deleting package by id: %v", err) } ctx.Error(http.StatusInternalServerError, "", err) + return } ctx.PlainText(http.StatusCreated, nil) diff --git a/routers/api/v1/packages/nuget/nuget.go b/routers/api/v1/packages/nuget/nuget.go index faaef9f15c1aa..2c40f41bf28c2 100644 --- a/routers/api/v1/packages/nuget/nuget.go +++ b/routers/api/v1/packages/nuget/nuget.go @@ -212,6 +212,7 @@ func UploadPackage(ctx *context.APIContext) { log.Error("Error deleting package by id: %v", err) } ctx.Error(http.StatusInternalServerError, "", err) + return } ctx.PlainText(http.StatusCreated, nil) diff --git a/routers/api/v1/packages/pypi/package.go b/routers/api/v1/packages/pypi/package.go new file mode 100644 index 0000000000000..8050d4f94d5b7 --- /dev/null +++ b/routers/api/v1/packages/pypi/package.go @@ -0,0 +1,53 @@ +// Copyright 2021 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 pypi + +import ( + "code.gitea.io/gitea/models" + pypi_module "code.gitea.io/gitea/modules/packages/pypi" + + jsoniter "github.com/json-iterator/go" +) + +// Package represents a package with NPM metadata +type Package struct { + *models.Package + Files []*models.PackageFile + Metadata *pypi_module.Metadata +} + +func intializePackages(packages []*models.Package) ([]*Package, error) { + pgs := make([]*Package, 0, len(packages)) + for _, p := range packages { + np, err := intializePackage(p) + if err != nil { + return nil, err + } + pgs = append(pgs, np) + } + return pgs, nil +} + +func intializePackage(p *models.Package) (*Package, error) { + var m *pypi_module.Metadata + err := jsoniter.Unmarshal([]byte(p.MetadataRaw), &m) + if err != nil { + return nil, err + } + if m == nil { + m = &pypi_module.Metadata{} + } + + pfs, err := p.GetFiles() + if err != nil { + return nil, err + } + + return &Package{ + Package: p, + Files: pfs, + Metadata: m, + }, nil +} diff --git a/routers/api/v1/packages/pypi/pypi.go b/routers/api/v1/packages/pypi/pypi.go new file mode 100644 index 0000000000000..1cfb156ce4397 --- /dev/null +++ b/routers/api/v1/packages/pypi/pypi.go @@ -0,0 +1,143 @@ +// Copyright 2021 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 pypi + +import ( + "crypto/sha256" + "fmt" + "io" + "net/http" + "regexp" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + pypi_module "code.gitea.io/gitea/modules/packages/pypi" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" + + package_service "code.gitea.io/gitea/services/packages" +) + +// https://www.python.org/dev/peps/pep-0503/#normalized-names +var normalizer = strings.NewReplacer(".", "-", "_", "-") +var nameMatcher = regexp.MustCompile(`\A[a-z0-9\.\-_]+\z`) + +// https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions +var versionMatcher = regexp.MustCompile(`^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$`) + +// PackageMetadata returns the metadata for a single package +func PackageMetadata(ctx *context.APIContext) { + packageName := normalizer.Replace(ctx.Params("id")) + + packages, err := models.GetPackagesByName(ctx.Repo.Repository.ID, models.PackagePyPI, packageName) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + if len(packages) == 0 { + ctx.Error(http.StatusNotFound, "", err) + return + } + + pypiPackages, err := intializePackages(packages) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + ctx.Data["RegistryURL"] = setting.AppURL + "api/v1/repos/" + ctx.Repo.Repository.FullName() + "/packages/pypi" + ctx.Data["Package"] = pypiPackages[0] + ctx.Data["Packages"] = pypiPackages + ctx.Render = templates.HTMLRenderer() + ctx.HTML(http.StatusOK, "api/packages/pypi/simple") +} + +// DownloadPackageContent serves the content of a package +func DownloadPackageContent(ctx *context.APIContext) { + packageName := normalizer.Replace(ctx.Params("id")) + packageVersion := ctx.Params("version") + filename := ctx.Params("filename") + + s, pf, err := package_service.GetPackageFileStream(ctx.Repo.Repository, models.PackagePyPI, packageName, packageVersion, filename) + if err != nil { + if err == models.ErrPackageNotExist || err == models.ErrPackageFileNotExist { + ctx.Error(http.StatusNotFound, "", err) + return + } + ctx.Error(http.StatusInternalServerError, "", err) + return + } + defer s.Close() + + ctx.ServeStream(s, pf.Name) +} + +// UploadPackageFile adds a file to the package. If the package does not exist, it gets created. +func UploadPackageFile(ctx *context.APIContext) { + file, fileHeader, err := ctx.Req.FormFile("content") + if err != nil { + ctx.Error(http.StatusBadRequest, "", err) + return + } + defer file.Close() + + h256 := sha256.New() + if _, err := io.Copy(h256, file); err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + if !strings.EqualFold(ctx.Req.FormValue("sha256_digest"), fmt.Sprintf("%x", h256.Sum(nil))) { + ctx.Error(http.StatusBadRequest, "", "hash mismatch") + return + } + + if _, err := file.Seek(0, io.SeekStart); err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + packageName := normalizer.Replace(ctx.Req.FormValue("name")) + packageVersion := ctx.Req.FormValue("version") + if !nameMatcher.MatchString(packageName) || !versionMatcher.MatchString(packageVersion) { + ctx.Error(http.StatusBadRequest, "", "invalid name or version") + return + } + + metadata := &pypi_module.Metadata{ + Author: ctx.Req.FormValue("author"), + Description: ctx.Req.FormValue("description"), + Summary: ctx.Req.FormValue("summary"), + ProjectURL: ctx.Req.FormValue("home_page"), + License: ctx.Req.FormValue("license"), + RequiresPython: ctx.Req.FormValue("requires_python"), + } + + p, err := package_service.CreatePackage( + ctx.User, + ctx.Repo.Repository, + models.PackagePyPI, + packageName, + packageVersion, + metadata, + true, + ) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + _, err = package_service.AddFileToPackage(p, fileHeader.Filename, fileHeader.Size, file) + if err != nil { + if err == models.ErrDuplicatePackageFile { + ctx.Error(http.StatusBadRequest, "", err) + return + } + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + ctx.PlainText(http.StatusCreated, nil) +} diff --git a/templates/api/packages/pypi/simple.tmpl b/templates/api/packages/pypi/simple.tmpl new file mode 100644 index 0000000000000..f3ed056505179 --- /dev/null +++ b/templates/api/packages/pypi/simple.tmpl @@ -0,0 +1,15 @@ + + + + Links for {{.Package.Name}} + + +

Links for {{.Package.Name}}

+ {{range .Packages}} + {{$p := .}} + {{range .Files}} + {{.Name}}
+ {{end}} + {{end}} + + \ No newline at end of file From 06790f91d00a2f9fc0fef41d99aa2c2e95d3dbba Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 19 Jul 2021 21:32:34 +0000 Subject: [PATCH 010/130] Summary is deprecated. --- modules/packages/nuget/metadata.go | 7 +++---- modules/packages/nuget/metadata_test.go | 3 --- routers/api/v1/packages/nuget/api.go | 4 ---- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index ac6203f049b0d..7704ddd20d164 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -36,7 +36,6 @@ type Metadata struct { ID string `json:"-"` Version string `json:"-"` Description string `json:"description"` - Summary string `json:"summary"` ReleaseNotes string `json:"release_notes"` Authors string `json:"authors"` ProjectURL string `json:"project_url"` @@ -58,7 +57,6 @@ type nuspecPackage struct { RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"` ProjectURL string `xml:"projectUrl"` Description string `xml:"description"` - Summary string `xml:"summary"` ReleaseNotes string `xml:"releaseNotes"` Repository struct { URL string `xml:"url,attr"` @@ -123,7 +121,6 @@ func ParseNuspecMetaData(r io.Reader) (*Metadata, error) { ID: p.Metadata.ID, Version: v.String(), Description: p.Metadata.Description, - Summary: p.Metadata.Summary, ReleaseNotes: p.Metadata.ReleaseNotes, Authors: p.Metadata.Authors, ProjectURL: p.Metadata.ProjectURL, @@ -141,7 +138,9 @@ func ParseNuspecMetaData(r io.Reader) (*Metadata, error) { Version: dep.Version, }) } - m.Dependencies[group.TargetFramework] = deps + if len(deps) > 0 { + m.Dependencies[group.TargetFramework] = deps + } } return m, nil } diff --git a/modules/packages/nuget/metadata_test.go b/modules/packages/nuget/metadata_test.go index 6e50256dbcf41..40a00a190e4f9 100644 --- a/modules/packages/nuget/metadata_test.go +++ b/modules/packages/nuget/metadata_test.go @@ -19,7 +19,6 @@ const ( authors = "Gitea Authors" projectURL = "https://gitea.io" description = "Package Description" - summary = "Package Summary" releaseNotes = "Package Release Notes" repositoryURL = "https://gitea.io/gitea/gitea" targetFramework = ".NETStandard2.1" @@ -36,7 +35,6 @@ const nuspecContent = ` true ` + projectURL + ` ` + description + ` - ` + summary + ` ` + releaseNotes + ` @@ -124,7 +122,6 @@ func TestParseNuspecMetaData(t *testing.T) { assert.Equal(t, authors, m.Authors) assert.Equal(t, projectURL, m.ProjectURL) assert.Equal(t, description, m.Description) - assert.Equal(t, summary, m.Summary) assert.Equal(t, releaseNotes, m.ReleaseNotes) assert.Equal(t, repositoryURL, m.RepositoryURL) assert.Len(t, m.Dependencies, 1) diff --git a/routers/api/v1/packages/nuget/api.go b/routers/api/v1/packages/nuget/api.go index b981cc98256b7..1e8c372a54a42 100644 --- a/routers/api/v1/packages/nuget/api.go +++ b/routers/api/v1/packages/nuget/api.go @@ -71,7 +71,6 @@ type CatalogEntry struct { ID string `json:"id"` Version string `json:"version"` Description string `json:"description"` - Summary string `json:"summary"` ReleaseNotes string `json:"releaseNotes"` Authors string `json:"authors"` RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"` @@ -125,7 +124,6 @@ func createRegistrationIndexPageItem(l *linkBuilder, p *Package) *RegistrationIn ID: p.Name, Version: p.Version, Description: p.Metadata.Description, - Summary: p.Metadata.Summary, ReleaseNotes: p.Metadata.ReleaseNotes, Authors: p.Metadata.Authors, ProjectURL: p.Metadata.ProjectURL, @@ -202,7 +200,6 @@ type SearchResult struct { Version string `json:"version"` Versions []*SearchResultVersion `json:"versions"` Description string `json:"description"` - Summary string `json:"summary"` Authors string `json:"authors"` ProjectURL string `json:"projectURL"` RegistrationIndexURL string `json:"registration"` @@ -258,7 +255,6 @@ func createSearchResult(l *linkBuilder, packages []*Package) *SearchResult { Version: latest.Version, Versions: versions, Description: latest.Metadata.Description, - Summary: latest.Metadata.Summary, Authors: latest.Metadata.Authors, ProjectURL: latest.Metadata.ProjectURL, RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Name), From 338dee3eb9180c9d172495c9f01823f47f4392ac Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 20 Jul 2021 19:21:45 +0000 Subject: [PATCH 011/130] Changed npm name. --- integrations/api_packages_npm_test.go | 2 +- models/package.go | 18 ++++++- modules/packages/pypi/metadata.go | 13 ++--- routers/api/v1/packages/generic/generic.go | 4 +- routers/api/v1/packages/npm/npm.go | 6 +-- routers/api/v1/packages/nuget/nuget.go | 4 +- routers/api/v1/packages/pypi/pypi.go | 2 +- services/packages/packages.go | 60 ++++++++++++++++++++-- 8 files changed, 89 insertions(+), 20 deletions(-) diff --git a/integrations/api_packages_npm_test.go b/integrations/api_packages_npm_test.go index d49e0e6900c7a..cc3443d1a19ec 100644 --- a/integrations/api_packages_npm_test.go +++ b/integrations/api_packages_npm_test.go @@ -63,7 +63,7 @@ func TestPackageNPM(t *testing.T) { req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusCreated) - ps, err := models.GetPackagesByRepositoryAndType(repository.ID, models.PackageNPM) + ps, err := models.GetPackagesByRepositoryAndType(repository.ID, models.PackageNpm) assert.NoError(t, err) assert.Len(t, ps, 1) assert.Equal(t, packageName, ps[0].Name) diff --git a/models/package.go b/models/package.go index 13dcdbb401d68..c882363514e3e 100644 --- a/models/package.go +++ b/models/package.go @@ -20,11 +20,27 @@ type PackageType int const ( PackageGeneric PackageType = iota PackageNuGet // 1 - PackageNPM // 2 + PackageNpm // 2 PackageMaven // 3 PackagePyPI // 4 ) +func (pt PackageType) String() string { + switch pt { + case PackageGeneric: + return "Generic" + case PackageNuGet: + return "NuGet" + case PackageNpm: + return "npm" + case PackageMaven: + return "Maven" + case PackagePyPI: + return "PyPI" + } + return "" +} + var ( // ErrDuplicatePackage indicates a duplicated package error ErrDuplicatePackage = errors.New("Package does exist already") diff --git a/modules/packages/pypi/metadata.go b/modules/packages/pypi/metadata.go index e3998af794789..27fc585975644 100644 --- a/modules/packages/pypi/metadata.go +++ b/modules/packages/pypi/metadata.go @@ -6,10 +6,11 @@ package pypi // Metadata represents the metadata of a PyPI package type Metadata struct { - Author string `json:"author"` - Description string `json:"description"` - Summary string `json:"summary"` - ProjectURL string `json:"project_url"` - License string `json:"license"` - RequiresPython string `json:"requires_python"` + Author string `json:"author"` + Description string `json:"description"` + LongDescription string `json:"long_description"` + Summary string `json:"summary"` + ProjectURL string `json:"project_url"` + License string `json:"license"` + RequiresPython string `json:"requires_python"` } diff --git a/routers/api/v1/packages/generic/generic.go b/routers/api/v1/packages/generic/generic.go index 89da2a19f43c2..0c742f0abf4c0 100644 --- a/routers/api/v1/packages/generic/generic.go +++ b/routers/api/v1/packages/generic/generic.go @@ -30,7 +30,7 @@ func DownloadPackageContent(ctx *context.APIContext) { return } - s, pf, err := package_service.GetPackageFileStream(ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion, filename) + s, pf, err := package_service.GetFileStreamByPackageNameAndVersion(ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion, filename) if err != nil { if err == models.ErrPackageNotExist || err == models.ErrPackageFileNotExist { ctx.Error(http.StatusNotFound, "", err) @@ -110,7 +110,7 @@ func DeletePackage(ctx *context.APIContext) { return } - err = package_service.DeletePackage(ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion) + err = package_service.DeletePackageByNameAndVersion(ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion) if err != nil { if err == models.ErrPackageNotExist { ctx.Error(http.StatusNotFound, "", err) diff --git a/routers/api/v1/packages/npm/npm.go b/routers/api/v1/packages/npm/npm.go index 440f5445c4519..75365943dc02d 100644 --- a/routers/api/v1/packages/npm/npm.go +++ b/routers/api/v1/packages/npm/npm.go @@ -26,7 +26,7 @@ func PackageMetadata(ctx *context.APIContext) { return } - packages, err := models.GetPackagesByName(ctx.Repo.Repository.ID, models.PackageNPM, packageName) + packages, err := models.GetPackagesByName(ctx.Repo.Repository.ID, models.PackageNpm, packageName) if err != nil { ctx.Error(http.StatusInternalServerError, "", err) return @@ -60,7 +60,7 @@ func DownloadPackageContent(ctx *context.APIContext) { packageVersion := ctx.Params("version") filename := ctx.Params("filename") - s, pf, err := package_service.GetPackageFileStream(ctx.Repo.Repository, models.PackageNPM, packageName, packageVersion, filename) + s, pf, err := package_service.GetFileStreamByPackageNameAndVersion(ctx.Repo.Repository, models.PackageNpm, packageName, packageVersion, filename) if err != nil { if err == models.ErrPackageNotExist || err == models.ErrPackageFileNotExist { ctx.Error(http.StatusNotFound, "", err) @@ -85,7 +85,7 @@ func UploadPackage(ctx *context.APIContext) { p, err := package_service.CreatePackage( ctx.User, ctx.Repo.Repository, - models.PackageNPM, + models.PackageNpm, npmPackage.Name, npmPackage.Version, npmPackage.Metadata, diff --git a/routers/api/v1/packages/nuget/nuget.go b/routers/api/v1/packages/nuget/nuget.go index 2c40f41bf28c2..fa18f8e3380d4 100644 --- a/routers/api/v1/packages/nuget/nuget.go +++ b/routers/api/v1/packages/nuget/nuget.go @@ -144,7 +144,7 @@ func DownloadPackageContent(ctx *context.APIContext) { packageVersion := ctx.Params("version") filename := ctx.Params("filename") - s, pf, err := package_service.GetPackageFileStream(ctx.Repo.Repository, models.PackageNuGet, packageName, packageVersion, filename) + s, pf, err := package_service.GetFileStreamByPackageNameAndVersion(ctx.Repo.Repository, models.PackageNuGet, packageName, packageVersion, filename) if err != nil { if err == models.ErrPackageNotExist || err == models.ErrPackageFileNotExist { ctx.Error(http.StatusNotFound, "", err) @@ -224,7 +224,7 @@ func DeletePackage(ctx *context.APIContext) { packageName := ctx.Params("id") packageVersion := ctx.Params("version") - err := package_service.DeletePackage(ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion) + err := package_service.DeletePackageByNameAndVersion(ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion) if err != nil { if err == models.ErrPackageNotExist { ctx.Error(http.StatusNotFound, "", err) diff --git a/routers/api/v1/packages/pypi/pypi.go b/routers/api/v1/packages/pypi/pypi.go index 1cfb156ce4397..f2a8bd5fe6ec3 100644 --- a/routers/api/v1/packages/pypi/pypi.go +++ b/routers/api/v1/packages/pypi/pypi.go @@ -61,7 +61,7 @@ func DownloadPackageContent(ctx *context.APIContext) { packageVersion := ctx.Params("version") filename := ctx.Params("filename") - s, pf, err := package_service.GetPackageFileStream(ctx.Repo.Repository, models.PackagePyPI, packageName, packageVersion, filename) + s, pf, err := package_service.GetFileStreamByPackageNameAndVersion(ctx.Repo.Repository, models.PackagePyPI, packageName, packageVersion, filename) if err != nil { if err == models.ErrPackageNotExist || err == models.ErrPackageFileNotExist { ctx.Error(http.StatusNotFound, "", err) diff --git a/services/packages/packages.go b/services/packages/packages.go index b8b88fd278ef6..8550d58ca27db 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -108,8 +108,8 @@ func AddFileToPackage(p *models.Package, filename string, size int64, r io.Reade return pf, nil } -// DeletePackage deletes a package and all associated files -func DeletePackage(repository *models.Repository, packageType models.PackageType, name, version string) error { +// DeletePackageByNameAndVersion deletes a package and all associated files +func DeletePackageByNameAndVersion(repository *models.Repository, packageType models.PackageType, name, version string) error { log.Trace("Deleting package: %v, %v, %s, %s", repository.ID, packageType, name, version) p, err := models.GetPackageByNameAndVersion(repository.ID, packageType, name, version) @@ -121,6 +121,30 @@ func DeletePackage(repository *models.Repository, packageType models.PackageType return err } + return deletePackage(p) +} + +// DeletePackageByID deletes a package and all associated files +func DeletePackageByID(repository *models.Repository, packageID int64) error { + log.Trace("Deleting package: %v, %v", repository.ID, packageID) + + p, err := models.GetPackageByID(packageID) + if err != nil { + if err == models.ErrPackageNotExist { + return err + } + log.Error("Error getting package: %v", err) + return err + } + + if p.RepoID != repository.ID { + return models.ErrPackageNotExist + } + + return deletePackage(p) +} + +func deletePackage(p *models.Package) error { pfs, err := p.GetFiles() if err != nil { log.Error("Error getting package files: %v", err) @@ -143,15 +167,43 @@ func DeletePackage(repository *models.Repository, packageType models.PackageType return nil } -// GetPackageFileStream returns the content of the specific package file -func GetPackageFileStream(repository *models.Repository, packageType models.PackageType, name, version, filename string) (io.ReadCloser, *models.PackageFile, error) { +// GetFileStreamByPackageNameAndVersion returns the content of the specific package file +func GetFileStreamByPackageNameAndVersion(repository *models.Repository, packageType models.PackageType, name, version, filename string) (io.ReadCloser, *models.PackageFile, error) { log.Trace("Getting package file stream: %v, %v, %s, %s, %s", repository.ID, packageType, name, version, filename) p, err := models.GetPackageByNameAndVersion(repository.ID, packageType, name, version) if err != nil { + if err == models.ErrPackageNotExist { + return nil, nil, err + } + log.Error("Error getting package: %v", err) + return nil, nil, err + } + + return getPackageFileStream(p, filename) +} + +// GetFileStreamByPackageID returns the content of the specific package file +func GetFileStreamByPackageID(repository *models.Repository, packageID int64, filename string) (io.ReadCloser, *models.PackageFile, error) { + log.Trace("Getting package file stream: %v, %v, %s", repository.ID, packageID, filename) + + p, err := models.GetPackageByID(packageID) + if err != nil { + if err == models.ErrPackageNotExist { + return nil, nil, err + } + log.Error("Error getting package: %v", err) return nil, nil, err } + if p.RepoID != repository.ID { + return nil, nil, models.ErrPackageNotExist + } + + return getPackageFileStream(p, filename) +} + +func getPackageFileStream(p *models.Package, filename string) (io.ReadCloser, *models.PackageFile, error) { pf, err := p.GetFileByName(filename) if err != nil { return nil, nil, err From 0544b22e0df0a53c1ad2a6565a24f784bbf149c9 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 20 Jul 2021 19:38:44 +0000 Subject: [PATCH 012/130] Sanitize project url. --- modules/packages/maven/metadata.go | 6 ++++++ modules/packages/npm/creator.go | 6 ++++++ modules/packages/nuget/metadata.go | 6 ++++++ routers/api/v1/packages/pypi/pypi.go | 19 +++++++++++++------ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/modules/packages/maven/metadata.go b/modules/packages/maven/metadata.go index 09df4c4e75804..6e0dd88a6add8 100644 --- a/modules/packages/maven/metadata.go +++ b/modules/packages/maven/metadata.go @@ -7,6 +7,8 @@ package maven import ( "encoding/xml" "io" + + "code.gitea.io/gitea/modules/validation" ) // Metadata represents the metadata of a Maven package @@ -55,6 +57,10 @@ func ParsePackageMetaData(r io.Reader) (*Metadata, error) { return nil, err } + if !validation.IsValidURL(pom.URL) { + pom.URL = "" + } + licenses := make([]string, 0, len(pom.Licenses)) for _, l := range pom.Licenses { if l.Name != "" { diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index 8eff6dac1140e..25fd30c57431e 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -16,6 +16,8 @@ import ( "strings" "time" + "code.gitea.io/gitea/modules/validation" + "github.com/hashicorp/go-version" jsoniter "github.com/json-iterator/go" ) @@ -134,6 +136,10 @@ func ParsePackage(r io.Reader) (*Package, error) { return nil, ErrInvalidPackageVersion } + if !validation.IsValidURL(meta.Homepage) { + meta.Homepage = "" + } + p := &Package{ Name: meta.Name, Version: meta.Version, diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index 7704ddd20d164..7d2c3e293cbc5 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -13,6 +13,8 @@ import ( "regexp" "strings" + "code.gitea.io/gitea/modules/validation" + "github.com/hashicorp/go-version" ) @@ -117,6 +119,10 @@ func ParseNuspecMetaData(r io.Reader) (*Metadata, error) { return nil, ErrNuspecInvalidVersion } + if !validation.IsValidURL(p.Metadata.ProjectURL) { + p.Metadata.ProjectURL = "" + } + m := &Metadata{ ID: p.Metadata.ID, Version: v.String(), diff --git a/routers/api/v1/packages/pypi/pypi.go b/routers/api/v1/packages/pypi/pypi.go index f2a8bd5fe6ec3..7c4e6e938804b 100644 --- a/routers/api/v1/packages/pypi/pypi.go +++ b/routers/api/v1/packages/pypi/pypi.go @@ -17,6 +17,7 @@ import ( pypi_module "code.gitea.io/gitea/modules/packages/pypi" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/validation" package_service "code.gitea.io/gitea/services/packages" ) @@ -106,13 +107,19 @@ func UploadPackageFile(ctx *context.APIContext) { return } + projectURL := ctx.Req.FormValue("home_page") + if !validation.IsValidURL(projectURL) { + projectURL = "" + } + metadata := &pypi_module.Metadata{ - Author: ctx.Req.FormValue("author"), - Description: ctx.Req.FormValue("description"), - Summary: ctx.Req.FormValue("summary"), - ProjectURL: ctx.Req.FormValue("home_page"), - License: ctx.Req.FormValue("license"), - RequiresPython: ctx.Req.FormValue("requires_python"), + Author: ctx.Req.FormValue("author"), + Description: ctx.Req.FormValue("description"), + LongDescription: ctx.Req.FormValue("long_description"), + Summary: ctx.Req.FormValue("summary"), + ProjectURL: projectURL, + License: ctx.Req.FormValue("license"), + RequiresPython: ctx.Req.FormValue("requires_python"), } p, err := package_service.CreatePackage( From 5a3c5612f016bea7a2dc5f4befb96aaa70b4a0e3 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 20 Jul 2021 20:38:15 +0000 Subject: [PATCH 013/130] Allow only scoped packages. --- modules/packages/npm/creator.go | 9 ++++---- modules/packages/npm/creator_test.go | 34 ++++++++++++++++------------ modules/packages/npm/metadata.go | 2 ++ 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index 25fd30c57431e..33b99dc87bf2d 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -35,7 +35,7 @@ var ( ErrInvalidIntegrity = errors.New("Failed to validate integrity") ) -var nameMatch = regexp.MustCompile(`\A(?:@([^/~'!\(\)\*]+?)[/])?([^/~'!\(\)\*]+?)\z`) +var nameMatch = regexp.MustCompile(`\A(@[^\/~'!\(\)\*]+?)[\/]([^_.][^\/~'!\(\)\*]+)\z`) // Package represents a NPM package type Package struct { @@ -136,6 +136,8 @@ func ParsePackage(r io.Reader) (*Package, error) { return nil, ErrInvalidPackageVersion } + nameParts := strings.SplitN(meta.Name, "/", 2) + if !validation.IsValidURL(meta.Homepage) { meta.Homepage = "" } @@ -144,6 +146,8 @@ func ParsePackage(r io.Reader) (*Package, error) { Name: meta.Name, Version: meta.Version, Metadata: Metadata{ + Scope: nameParts[0], + Name: nameParts[1], Description: meta.Description, Author: meta.Author.Name, License: meta.License, @@ -210,8 +214,5 @@ func validateName(name string) bool { if len(name) == 0 || len(name) > 214 { return false } - if name[0] == '.' || name[0] == '_' { - return false - } return nameMatch.MatchString(name) } diff --git a/modules/packages/npm/creator_test.go b/modules/packages/npm/creator_test.go index aba8fa9c0057f..6abedf35913ff 100644 --- a/modules/packages/npm/creator_test.go +++ b/modules/packages/npm/creator_test.go @@ -37,22 +37,28 @@ func TestParsePackage(t *testing.T) { }) t.Run("InvalidPackageName", func(t *testing.T) { - name := " test " - b, _ := jsoniter.Marshal(packageUpload{ - PackageMetadata: PackageMetadata{ - ID: name, - Name: name, - Versions: map[string]*PackageMetadataVersion{ - packageVersion: { - Name: name, + test := func(t *testing.T, name string) { + b, _ := jsoniter.Marshal(packageUpload{ + PackageMetadata: PackageMetadata{ + ID: name, + Name: name, + Versions: map[string]*PackageMetadataVersion{ + packageVersion: { + Name: name, + }, }, }, - }, - }) - - p, err := ParsePackage(bytes.NewReader(b)) - assert.Nil(t, p) - assert.ErrorIs(t, err, ErrInvalidPackageName) + }) + + p, err := ParsePackage(bytes.NewReader(b)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidPackageName) + } + + test(t, " test ") + test(t, "invalid/scope") + test(t, "@invalid/_name") + test(t, "@invalid/.name") }) t.Run("InvalidPackageVersion", func(t *testing.T) { diff --git a/modules/packages/npm/metadata.go b/modules/packages/npm/metadata.go index a2e40b5b6c9cb..f0c9586ccc516 100644 --- a/modules/packages/npm/metadata.go +++ b/modules/packages/npm/metadata.go @@ -6,6 +6,8 @@ package npm // Metadata represents the metadata of a NPM package type Metadata struct { + Scope string `json:"scope"` + Name string `json:"name"` Description string `json:"description"` Author string `json:"author"` License string `json:"license"` From ea36a75f9eb0bd6ff98dc6d565de86e6140b1359 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 20 Jul 2021 22:16:42 +0000 Subject: [PATCH 014/130] Added user interface. --- models/package.go | 67 +++++++ models/repo_unit.go | 2 +- models/unit.go | 14 ++ modules/context/context.go | 1 + modules/context/repo.go | 1 + modules/packages/npm/creator_test.go | 2 +- modules/setting/setting.go | 2 + modules/templates/helper.go | 12 +- options/locale/locale_en-US.ini | 35 ++++ routers/api/v1/api.go | 16 +- routers/web/repo/packages.go | 171 ++++++++++++++++++ routers/web/repo/setting.go | 9 + routers/web/web.go | 11 ++ services/forms/repo_form.go | 1 + templates/repo/header.tmpl | 6 + templates/repo/packages/content/generic.tmpl | 12 ++ templates/repo/packages/content/maven.tmpl | 59 ++++++ templates/repo/packages/content/npm.tmpl | 38 ++++ templates/repo/packages/content/nuget.tmpl | 36 ++++ templates/repo/packages/content/pypi.tmpl | 24 +++ templates/repo/packages/list.tmpl | 55 ++++++ templates/repo/packages/metadata/generic.tmpl | 0 templates/repo/packages/metadata/maven.tmpl | 10 + templates/repo/packages/metadata/npm.tmpl | 5 + templates/repo/packages/metadata/nuget.tmpl | 4 + templates/repo/packages/metadata/pypi.tmpl | 6 + templates/repo/packages/view.tmpl | 79 ++++++++ templates/repo/settings/options.tmpl | 13 ++ 28 files changed, 680 insertions(+), 11 deletions(-) create mode 100644 routers/web/repo/packages.go create mode 100644 templates/repo/packages/content/generic.tmpl create mode 100644 templates/repo/packages/content/maven.tmpl create mode 100644 templates/repo/packages/content/npm.tmpl create mode 100644 templates/repo/packages/content/nuget.tmpl create mode 100644 templates/repo/packages/content/pypi.tmpl create mode 100644 templates/repo/packages/list.tmpl create mode 100644 templates/repo/packages/metadata/generic.tmpl create mode 100644 templates/repo/packages/metadata/maven.tmpl create mode 100644 templates/repo/packages/metadata/npm.tmpl create mode 100644 templates/repo/packages/metadata/nuget.tmpl create mode 100644 templates/repo/packages/metadata/pypi.tmpl create mode 100644 templates/repo/packages/view.tmpl diff --git a/models/package.go b/models/package.go index c882363514e3e..c2de2e5c404c5 100644 --- a/models/package.go +++ b/models/package.go @@ -8,6 +8,7 @@ import ( "errors" "strings" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "xorm.io/builder" @@ -57,6 +58,7 @@ type Package struct { ID int64 `xorm:"pk autoincr"` RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` CreatorID int64 + Creator *User `xorm:"-"` Type PackageType `xorm:"UNIQUE(s) INDEX NOT NULL"` Name string LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` @@ -67,6 +69,16 @@ type Package struct { UpdatedUnix timeutil.TimeStamp `xorm:"updated"` } +// LoadCreator loads poster +func (p *Package) LoadCreator() error { + if p.Creator == nil { + var err error + p.Creator, err = getUserByID(x, p.CreatorID) + return err + } + return nil +} + // PackageFile represents files associated with a package type PackageFile struct { ID int64 `xorm:"pk autoincr"` @@ -166,6 +178,61 @@ func DeletePackagesByRepositoryID(repositoryID int64) error { return nil } +// PackageSearchOptions are options for GetLatestPackagesGrouped +type PackageSearchOptions struct { + RepoID int64 + Page int + Query string + Type string +} + +// GetLatestPackagesGrouped returns a list of all packages in their latest version of the repository +func GetLatestPackagesGrouped(opts PackageSearchOptions) ([]*Package, int64, error) { + var cond builder.Cond = builder.Eq{"package.repo_id": opts.RepoID} + cond = cond.And(builder.Expr("p2.id IS NULL")) + + switch opts.Type { + case "generic": + cond = cond.And(builder.Eq{"package.type": PackageGeneric}) + case "nuget": + cond = cond.And(builder.Eq{"package.type": PackageNuGet}) + case "npm": + cond = cond.And(builder.Eq{"package.type": PackageNpm}) + case "maven": + cond = cond.And(builder.Eq{"package.type": PackageMaven}) + case "pypi": + cond = cond.And(builder.Eq{"package.type": PackagePyPI}) + } + + if opts.Query != "" { + cond = cond.And(builder.Like{"package.lower_name", strings.ToLower(opts.Query)}) + } + + sess := x.Where(cond). + Table("package"). + Join("left", "package p2", "package.repo_id = p2.repo_id AND package.type = p2.type AND package.lower_name = p2.lower_name AND package.version < p2.version") + + if opts.Page > 0 { + sess = sess.Limit(setting.UI.PackagesPagingNum, (opts.Page-1)*setting.UI.PackagesPagingNum) + } + + packages := make([]*Package, 0, setting.UI.PackagesPagingNum) + count, err := sess.FindAndCount(&packages) + return packages, count, err +} + +// GetPackageByID returns the package with the specific id +func GetPackageByID(packageID int64) (*Package, error) { + p := &Package{} + has, err := x.ID(packageID).Get(p) + if err != nil { + return nil, err + } else if !has { + return nil, ErrPackageNotExist + } + return p, nil +} + // GetPackagesByRepository returns all packages of a repository func GetPackagesByRepository(repositoryID int64) ([]*Package, error) { packages := make([]*Package, 0, 10) diff --git a/models/repo_unit.go b/models/repo_unit.go index d8060d16a03c9..338b9ea4157ab 100644 --- a/models/repo_unit.go +++ b/models/repo_unit.go @@ -153,7 +153,7 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { switch colName { case "type": switch UnitType(Cell2Int64(val)) { - case UnitTypeCode, UnitTypeReleases, UnitTypeWiki, UnitTypeProjects: + case UnitTypeCode, UnitTypeReleases, UnitTypeWiki, UnitTypeProjects, UnitTypePackages: r.Config = new(UnitConfig) case UnitTypeExternalWiki: r.Config = new(ExternalWikiConfig) diff --git a/models/unit.go b/models/unit.go index 939deba574824..6b2f04004ba8f 100644 --- a/models/unit.go +++ b/models/unit.go @@ -25,6 +25,7 @@ const ( UnitTypeExternalWiki // 6 ExternalWiki UnitTypeExternalTracker // 7 ExternalTracker UnitTypeProjects // 8 Kanban board + UnitTypePackages // 9 Packages ) // Value returns integer value for unit type @@ -50,6 +51,8 @@ func (u UnitType) String() string { return "UnitTypeExternalTracker" case UnitTypeProjects: return "UnitTypeProjects" + case UnitTypePackages: + return "UnitTypePackages" } return fmt.Sprintf("Unknown UnitType %d", u) } @@ -72,6 +75,7 @@ var ( UnitTypeExternalWiki, UnitTypeExternalTracker, UnitTypeProjects, + UnitTypePackages, } // DefaultRepoUnits contains the default unit types @@ -82,6 +86,7 @@ var ( UnitTypeReleases, UnitTypeWiki, UnitTypeProjects, + UnitTypePackages, } // NotAllowedDefaultRepoUnits contains units that can't be default @@ -255,6 +260,14 @@ var ( 5, } + UnitPackages = Unit{ + UnitTypePackages, + "repo.packages", + "/packages", + "repo.packages.desc", + 6, + } + // Units contains all the units Units = map[UnitType]Unit{ UnitTypeCode: UnitCode, @@ -265,6 +278,7 @@ var ( UnitTypeWiki: UnitWiki, UnitTypeExternalWiki: UnitExternalWiki, UnitTypeProjects: UnitProjects, + UnitTypePackages: UnitPackages, } ) diff --git a/modules/context/context.go b/modules/context/context.go index 211ac14ec1c0b..e48a84bd78138 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -782,6 +782,7 @@ func Contexter() func(next http.Handler) http.Handler { ctx.Data["UnitWikiGlobalDisabled"] = models.UnitTypeWiki.UnitGlobalDisabled() ctx.Data["UnitIssuesGlobalDisabled"] = models.UnitTypeIssues.UnitGlobalDisabled() ctx.Data["UnitPullsGlobalDisabled"] = models.UnitTypePullRequests.UnitGlobalDisabled() + ctx.Data["UnitPackagesGlobalDisabled"] = models.UnitTypePackages.UnitGlobalDisabled() ctx.Data["UnitProjectsGlobalDisabled"] = models.UnitTypeProjects.UnitGlobalDisabled() ctx.Data["i18n"] = locale diff --git a/modules/context/repo.go b/modules/context/repo.go index ea8323bdfc22b..07a3b64f99390 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -875,6 +875,7 @@ func UnitTypes() func(ctx *Context) { ctx.Data["UnitTypeExternalWiki"] = models.UnitTypeExternalWiki ctx.Data["UnitTypeExternalTracker"] = models.UnitTypeExternalTracker ctx.Data["UnitTypeProjects"] = models.UnitTypeProjects + ctx.Data["UnitTypePackages"] = models.UnitTypePackages } } diff --git a/modules/packages/npm/creator_test.go b/modules/packages/npm/creator_test.go index 6abedf35913ff..c294b695adaf0 100644 --- a/modules/packages/npm/creator_test.go +++ b/modules/packages/npm/creator_test.go @@ -49,7 +49,7 @@ func TestParsePackage(t *testing.T) { }, }, }) - + p, err := ParsePackage(bytes.NewReader(b)) assert.Nil(t, p) assert.ErrorIs(t, err, ErrInvalidPackageName) diff --git a/modules/setting/setting.go b/modules/setting/setting.go index e3da5796e4268..cf9bf79eb3936 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -198,6 +198,7 @@ var ( MembersPagingNum int FeedMaxCommitNum int FeedPagingNum int + PackagesPagingNum int GraphMaxCommitNum int CodeCommentLines int ReactionMaxUserNum int @@ -250,6 +251,7 @@ var ( MembersPagingNum: 20, FeedMaxCommitNum: 5, FeedPagingNum: 20, + PackagesPagingNum: 20, GraphMaxCommitNum: 100, CodeCommentLines: 4, ReactionMaxUserNum: 10, diff --git a/modules/templates/helper.go b/modules/templates/helper.go index f9b2dafd22a14..2369b74ae368f 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -30,6 +30,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/svg" @@ -164,7 +165,16 @@ func NewFuncMap() []template.FuncMap { "RenderEmojiPlain": emoji.ReplaceAliases, "ReactionToEmoji": ReactionToEmoji, "RenderNote": RenderNote, - "IsMultilineCommitMessage": IsMultilineCommitMessage, + "RenderMarkdownToHtml": func(input string) template.HTML { + output, err := markdown.RenderString(&markup.RenderContext{ + URLPrefix: setting.AppSubURL, + }, input) + if err != nil { + log.Error("RenderString: %v", err) + } + return template.HTML(output) + }, + "IsMultilineCommitMessage": IsMultilineCommitMessage, "ThemeColorMetaTag": func() string { return setting.UI.ThemeColorMetaTag }, diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 191cb5de6764c..4eec0a93c4812 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1492,6 +1492,40 @@ milestones.filter_sort.most_complete = Most complete milestones.filter_sort.most_issues = Most issues milestones.filter_sort.least_issues = Least issues +packages = Packages +packages.desc = Manage repository packages. +packages.filter.package_type = Type +packages.filter.package_type.all = All +packages.published_by = Published %[1]s by %[3]s +packages.installation = Installation +packages.about = About this package +packages.dependencies = Dependencies +packages.details = Details +packages.details.author = Author +packages.details.project_site = Project Site +packages.details.license = License +packages.assets = Assets +packages.versions = Versions +packages.versions.on = on +packages.delete = Delete package +packages.delete.notice = You are about to delete %s (%s). This operation is irreversible, are you sure? +packages.delete.success = The package has been deleted. +packages.generic.use = Download the package, for example with curl: +packages.generic.documentation = For more information on the generic registry, see the documentation. +packages.maven.registry = Setup this registry in your projects pom.xml file: +packages.maven.use = To use the package include the following in the dependencies block in the pom.xml file: +packages.maven.use_2 = Alternatively use this Maven command: +packages.maven.documentation = For more information on the Maven registry, see the documentation. +packages.nuget.registry = Setup this registry with the following command: +packages.nuget.use = Install the package with the following command: +packages.nuget.documentation = For more information on the NuGet registry, see the documentation. +packages.npm.registry = Setup this registry in your projects .npmrc file: +packages.npm.use = To install the package use the following command: +packages.npm.documentation = For more information on the npm registry, see the documentation. +packages.pypi.requires = Requires Python +packages.pypi.use = To install the package use the following command: +packages.pypi.documentation = For more information on the PyPI registry, see the documentation. + signing.will_sign = This commit will be signed with key '%s' signing.wont_sign.error = There was an error whilst checking if the commit could be signed signing.wont_sign.nokey = There is no key available to sign this commit @@ -1664,6 +1698,7 @@ settings.pulls.allow_rebase_merge_commit = Enable Rebasing with explicit merge c settings.pulls.allow_squash_commits = Enable Squashing to Merge Commits settings.pulls.allow_manual_merge = Enable Mark PR as manually merged settings.pulls.enable_autodetect_manual_merge = Enable autodetect manual merge (Note: In some special cases, misjudgments can occur) +settings.packages_desc = Enable Repository Packages Registry settings.projects_desc = Enable Repository Projects settings.admin_settings = Administrator Settings settings.admin_enable_health_check = Enable Repository Health Checks (git fsck) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 6b69f14671cce..f9de2ed23ee9f 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -980,16 +980,16 @@ func Routes() *web.Route { m.Group("/generic", func() { m.Group("/{packagename}/{packageversion}/{filename}", func() { m.Get("", generic.DownloadPackageContent) - m.Put("" /*reqRepoWriter(models.UnitTypePackage),*/, generic.UploadPackage) - m.Delete("" /*reqRepoWriter(models.UnitTypePackage),*/, generic.DeletePackage) + m.Put("", reqRepoWriter(models.UnitTypePackages), generic.UploadPackage) + m.Delete("", reqRepoWriter(models.UnitTypePackages), generic.DeletePackage) }) }, reqToken()) m.Group("/maven", func() { - m.Put("/*" /*reqRepoWriter(models.UnitTypePackage),*/, maven.UploadPackageFile) - m.Get("/*" /*reqRepoWriter(models.UnitTypePackage),*/, maven.DownloadPackageFile) + m.Put("/*", reqRepoWriter(models.UnitTypePackages), maven.UploadPackageFile) + m.Get("/*", reqRepoWriter(models.UnitTypePackages), maven.DownloadPackageFile) }, reqToken()) m.Group("/nuget", func() { - m.Put("/" /*reqRepoWriter(models.UnitTypePackage),*/, nuget.UploadPackage) + m.Put("/", reqRepoWriter(models.UnitTypePackages), nuget.UploadPackage) m.Get("/index.json", nuget.ServiceIndex) m.Get("/query", nuget.SearchService) m.Group("/registration/{id}", func() { @@ -999,7 +999,7 @@ func Routes() *web.Route { m.Group("/package/{id}", func() { m.Get("/index.json", nuget.EnumeratePackageVersions) m.Group("/{version}", func() { - m.Delete("/" /*reqRepoWriter(models.UnitTypePackage),*/, nuget.DeletePackage) + m.Delete("/", reqRepoWriter(models.UnitTypePackages), nuget.DeletePackage) m.Get("/{filename}", nuget.DownloadPackageContent) }) }) @@ -1007,12 +1007,12 @@ func Routes() *web.Route { m.Group("/npm", func() { m.Group("/{id}", func() { m.Get("", npm.PackageMetadata) - m.Put("" /*reqRepoWriter(models.UnitTypePackage),*/, npm.UploadPackage) + m.Put("", reqRepoWriter(models.UnitTypePackages), npm.UploadPackage) m.Get("/-/{version}/{filename}", npm.DownloadPackageContent) }) }, reqToken()) m.Group("/pypi", func() { - m.Post("/" /*reqRepoWriter(models.UnitTypePackage),*/, pypi.UploadPackageFile) + m.Post("/", reqRepoWriter(models.UnitTypePackages), pypi.UploadPackageFile) m.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageContent) m.Get("/simple/{id}", pypi.PackageMetadata) }, reqBasicAuth()) diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go new file mode 100644 index 0000000000000..d0168193e6a7f --- /dev/null +++ b/routers/web/repo/packages.go @@ -0,0 +1,171 @@ +// Copyright 2021 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 repo + +import ( + "net/http" + "sort" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/packages/maven" + "code.gitea.io/gitea/modules/packages/npm" + "code.gitea.io/gitea/modules/packages/nuget" + "code.gitea.io/gitea/modules/packages/pypi" + "code.gitea.io/gitea/modules/setting" + + package_service "code.gitea.io/gitea/services/packages" + + jsoniter "github.com/json-iterator/go" +) + +const ( + tplPackages base.TplName = "repo/packages/list" + tplPackagesView base.TplName = "repo/packages/view" +) + +// MustEnablePackages checks if packages are enabled +func MustEnablePackages(ctx *context.Context) { + if models.UnitTypePackages.UnitGlobalDisabled() || !ctx.Repo.CanRead(models.UnitTypePackages) { + ctx.NotFound("MustEnablePackages", nil) + } +} + +// Packages displays a list of all packages in the repository +func Packages(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("repo.packages") + + query := ctx.QueryTrim("q") + packageType := ctx.QueryTrim("package_type") + page := ctx.QueryInt("page") + if page <= 1 { + page = 1 + } + + repo := ctx.Repo.Repository + + packages, count, err := models.GetLatestPackagesGrouped(models.PackageSearchOptions{ + RepoID: repo.ID, + Query: query, + Page: page, + Type: packageType, + }) + if err != nil { + ctx.ServerError("GetLatestPackagesGrouped", err) + return + } + + for _, p := range packages { + if err := p.LoadCreator(); err != nil { + ctx.ServerError("LoadCreator", err) + return + } + } + + ctx.Data["Packages"] = packages + ctx.Data["IsPackagesPage"] = true + ctx.Data["Query"] = query + ctx.Data["PackageType"] = packageType + + pager := context.NewPagination(int(count), setting.UI.PackagesPagingNum, page, 5) + pager.AddParam(ctx, "q", "Query") + pager.AddParam(ctx, "package_type", "PackageType") + ctx.Data["Page"] = pager + + ctx.HTML(http.StatusOK, tplPackages) +} + +// ViewPackage displays a single package +func ViewPackage(ctx *context.Context) { + p, err := models.GetPackageByID(ctx.ParamsInt64(":id")) + if err != nil { + if err == models.ErrPackageNotExist { + ctx.NotFound("", nil) + } else { + ctx.ServerError("GetPackageByID", err) + } + return + } + if p.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound("", nil) + return + } + if err := p.LoadCreator(); err != nil { + ctx.ServerError("LoadCreator", err) + return + } + ctx.Data["Package"] = p + + var metadata interface{} + switch p.Type { + case models.PackageNuGet: + metadata = &nuget.Metadata{} + case models.PackageNpm: + metadata = &npm.Metadata{} + case models.PackageMaven: + metadata = &maven.Metadata{} + case models.PackagePyPI: + metadata = &pypi.Metadata{} + } + if metadata != nil { + if err := jsoniter.Unmarshal([]byte(p.MetadataRaw), &metadata); err != nil { + ctx.ServerError("Unmarshal", err) + return + } + } + ctx.Data["Metadata"] = metadata + + files, err := p.GetFiles() + if err != nil { + ctx.ServerError("GetFiles", err) + return + } + ctx.Data["Files"] = files + + otherVersions, err := models.GetPackagesByName(ctx.Repo.Repository.ID, p.Type, p.LowerName) + if err != nil { + ctx.ServerError("GetPackagesByName", err) + return + } + sort.Slice(otherVersions, func(i, j int) bool { + return otherVersions[i].Version > otherVersions[j].Version + }) + ctx.Data["OtherVersions"] = otherVersions + + ctx.Data["Repo"] = ctx.Repo.Repository + ctx.Data["CanWritePackages"] = ctx.Repo.Permission.CanWrite(models.UnitTypePackages) + ctx.Data["PageIsPackages"] = true + + ctx.HTML(http.StatusOK, tplPackagesView) +} + +// DeletePackagePost deletes a package +func DeletePackagePost(ctx *context.Context) { + err := package_service.DeletePackageByID(ctx.Repo.Repository, ctx.ParamsInt64(":id")) + if err != nil { + ctx.Flash.Error(err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("repo.packages.delete.success")) + } + + ctx.Redirect(ctx.Repo.RepoLink + "/packages") +} + +// DownloadPackageFile serves the content of a package file +func DownloadPackageFile(ctx *context.Context) { + s, pf, err := package_service.GetFileStreamByPackageID(ctx.Repo.Repository, ctx.ParamsInt64(":id"), ctx.Params(":filename")) + if err != nil { + if err == models.ErrPackageNotExist || err == models.ErrPackageFileNotExist { + ctx.NotFound("", err) + return + } + ctx.ServerError("GetFileStreamByPackageID", err) + return + } + defer s.Close() + + ctx.ServeStream(s, pf.Name) +} diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go index 5e8c2c5276251..26db4ca29a8f6 100644 --- a/routers/web/repo/setting.go +++ b/routers/web/repo/setting.go @@ -402,6 +402,15 @@ func SettingsPost(ctx *context.Context) { } } + if form.EnablePackages && !models.UnitTypePackages.UnitGlobalDisabled() { + units = append(units, models.RepoUnit{ + RepoID: repo.ID, + Type: models.UnitTypePackages, + }) + } else if !models.UnitTypePackages.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, models.UnitTypePackages) + } + if form.EnableProjects && !models.UnitTypeProjects.UnitGlobalDisabled() { units = append(units, models.RepoUnit{ RepoID: repo.ID, diff --git a/routers/web/web.go b/routers/web/web.go index 7a47f479c0da3..0a121ee0fcb4f 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -482,6 +482,8 @@ func RegisterRoutes(m *web.Route) { reqRepoIssuesOrPullsReader := context.RequireRepoReaderOr(models.UnitTypeIssues, models.UnitTypePullRequests) reqRepoProjectsReader := context.RequireRepoReader(models.UnitTypeProjects) reqRepoProjectsWriter := context.RequireRepoWriter(models.UnitTypeProjects) + reqRepoPackagesReader := context.RequireRepoReader(models.UnitTypePackages) + reqRepoPackagesWriter := context.RequireRepoWriter(models.UnitTypePackages) // ***** START: Organization ***** m.Group("/org", func() { @@ -837,6 +839,15 @@ func RegisterRoutes(m *web.Route) { m.Get("/milestones", reqRepoIssuesOrPullsReader, repo.Milestones) }, context.RepoRef()) + m.Group("/packages", func() { + m.Get("", repo.Packages) + m.Group("/{id}", func() { + m.Get("/", repo.ViewPackage) + m.Post("/", reqRepoPackagesWriter, repo.DeletePackagePost) + m.Get("/files/{filename}", repo.DownloadPackageFile) + }) + }, reqRepoPackagesReader, repo.MustEnablePackages) + m.Group("/projects", func() { m.Get("", repo.Projects) m.Get("/{id}", repo.ViewProject) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 71a83a8be36e2..45b94012a9166 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -141,6 +141,7 @@ type RepoSettingForm struct { TrackerURLFormat string TrackerIssueStyle string EnableCloseIssuesViaCommitInAnyBranch bool + EnablePackages bool EnableProjects bool EnablePulls bool PullsIgnoreWhitespace bool diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 49a651e6c5a86..fabb703744216 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -132,6 +132,12 @@ {{end}} + {{ if and (not .UnitPackagesGlobalDisabled) (.Permission.CanRead $.UnitTypePackages)}} + + {{svg "octicon-package"}} {{.i18n.Tr "repo.packages"}} + + {{ end }} + {{ if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects)}} {{svg "octicon-project"}} {{.i18n.Tr "repo.project_board"}} diff --git a/templates/repo/packages/content/generic.tmpl b/templates/repo/packages/content/generic.tmpl new file mode 100644 index 0000000000000..4c3969ff4799f --- /dev/null +++ b/templates/repo/packages/content/generic.tmpl @@ -0,0 +1,12 @@ +{{if eq .Package.Type 0}} +

{{.i18n.Tr "repo.packages.installation"}}

+
+
+ +
+ +
+
+ {{.i18n.Tr "repo.packages.generic.documentation" | Safe}} +
+{{end}} \ No newline at end of file diff --git a/templates/repo/packages/content/maven.tmpl b/templates/repo/packages/content/maven.tmpl new file mode 100644 index 0000000000000..2ffced4813f73 --- /dev/null +++ b/templates/repo/packages/content/maven.tmpl @@ -0,0 +1,59 @@ +{{if eq .Package.Type 3}} +

{{.i18n.Tr "repo.packages.installation"}}

+
+
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+ {{.i18n.Tr "repo.packages.maven.documentation" | Safe}} +
+ + {{if .Metadata.Description}} +

{{.i18n.Tr "repo.packages.about"}}

+
+ {{.Metadata.Description}} +
+ {{end}} + + {{if .Metadata.Dependencies}} +

{{.i18n.Tr "repo.packages.dependencies"}}

+
+ {{range .Metadata.Dependencies}} +
{{.GroupID}}:{{.ArtifactID}} ({{.Version}})
+ {{end}} +
+ {{end}} +{{end}} \ No newline at end of file diff --git a/templates/repo/packages/content/npm.tmpl b/templates/repo/packages/content/npm.tmpl new file mode 100644 index 0000000000000..8c0b5f66b2afa --- /dev/null +++ b/templates/repo/packages/content/npm.tmpl @@ -0,0 +1,38 @@ +{{if eq .Package.Type 2}} +

{{.i18n.Tr "repo.packages.installation"}}

+
+
+ +
+ +
+ +
+ +
+
+ {{.i18n.Tr "repo.packages.npm.documentation" | Safe}} +
+ + {{if or .Metadata.Description .Metadata.Readme}} +

{{.i18n.Tr "repo.packages.about"}}

+
+ {{if .Metadata.Readme}} +
+ {{RenderMarkdownToHtml .Metadata.Readme}} +
+ {{else if .Metadata.Description}} + {{.Metadata.Description}} + {{end}} +
+ {{end}} + + {{if .Metadata.Dependencies}} +

{{.i18n.Tr "repo.packages.dependencies"}}

+
+ {{range $dependency, $version := .Metadata.Dependencies}} +
{{$dependency}} ({{$version}})
+ {{end}} +
+ {{end}} +{{end}} \ No newline at end of file diff --git a/templates/repo/packages/content/nuget.tmpl b/templates/repo/packages/content/nuget.tmpl new file mode 100644 index 0000000000000..57a41aeeb5d4a --- /dev/null +++ b/templates/repo/packages/content/nuget.tmpl @@ -0,0 +1,36 @@ +{{if eq .Package.Type 1}} +

{{.i18n.Tr "repo.packages.installation"}}

+
+
+ +
+ +
+ +
+ +
+
+ {{.i18n.Tr "repo.packages.nuget.documentation" | Safe}} +
+ + {{if or .Metadata.Description .Metadata.ReleaseNotes}} +

{{.i18n.Tr "repo.packages.about"}}

+
+ {{if .Metadata.Description}}{{.Metadata.Description}}{{end}} + {{if .Metadata.ReleaseNotes}}{{Str2html .Metadata.ReleaseNotes}}{{end}} +
+ {{end}} + + {{if .Metadata.Dependencies}} +

{{.i18n.Tr "repo.packages.dependencies"}}

+
+ {{range $framework, $dependencies := .Metadata.Dependencies}} +
{{$framework}}
+ {{range $dependencies}} +
{{.ID}} ({{.Version}})
+ {{end}} + {{end}} +
+ {{end}} +{{end}} \ No newline at end of file diff --git a/templates/repo/packages/content/pypi.tmpl b/templates/repo/packages/content/pypi.tmpl new file mode 100644 index 0000000000000..dfbc6a8529e19 --- /dev/null +++ b/templates/repo/packages/content/pypi.tmpl @@ -0,0 +1,24 @@ +{{if eq .Package.Type 4}} +

{{.i18n.Tr "repo.packages.installation"}}

+
+
+ +
+ +
+
+ {{.i18n.Tr "repo.packages.pypi.documentation" | Safe}} +
+ + {{if or .Metadata.Description .Metadata.LongDescription .Metadata.Summary}} +

{{.i18n.Tr "repo.packages.about"}}

+
+

{{if .Metadata.Summary}}{{.Metadata.Summary}}{{end}}

+ {{if .Metadata.LongDescription}} + {{RenderMarkdownToHtml .Metadata.LongDescription}} + {{else if .Metadata.Description}} + {{RenderMarkdownToHtml .Metadata.Description}} + {{end}} +
+ {{end}} +{{end}} \ No newline at end of file diff --git a/templates/repo/packages/list.tmpl b/templates/repo/packages/list.tmpl new file mode 100644 index 0000000000000..cb342ab31ee8e --- /dev/null +++ b/templates/repo/packages/list.tmpl @@ -0,0 +1,55 @@ +{{template "base/head" .}} +
+ {{template "repo/header" .}} +
+ {{template "base/alert" .}} + +
+ {{range .Packages}} +
  • +
    +
    + {{.Name}} + {{.Version}} + {{.Type.String}} +
    +
    + {{ $timeStr := TimeSinceUnix .CreatedUnix $.Lang }} + {{$.i18n.Tr "repo.packages.published_by" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape) | Safe}} +
    +
    +
  • + {{end}} + {{template "base/paginate" .}} +
    +
    +
    +{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/repo/packages/metadata/generic.tmpl b/templates/repo/packages/metadata/generic.tmpl new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/templates/repo/packages/metadata/maven.tmpl b/templates/repo/packages/metadata/maven.tmpl new file mode 100644 index 0000000000000..0ea9adfd5b8d4 --- /dev/null +++ b/templates/repo/packages/metadata/maven.tmpl @@ -0,0 +1,10 @@ +{{if eq .Package.Type 3}} + {{if .Metadata.Name}}
    {{svg "octicon-note" 16 "mr-3"}} {{.Metadata.Name}}
    {{end}} + {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} + {{if .Metadata.Licenses}} +
    {{.i18n.Tr "repo.packages.details.license"}}:
    + {{range .Metadata.Licenses}} +
    {{svg "octicon-law" 16 "mr-3"}} {{.}}
    + {{end}} + {{end}} +{{end}} \ No newline at end of file diff --git a/templates/repo/packages/metadata/npm.tmpl b/templates/repo/packages/metadata/npm.tmpl new file mode 100644 index 0000000000000..0c37cc968cad4 --- /dev/null +++ b/templates/repo/packages/metadata/npm.tmpl @@ -0,0 +1,5 @@ +{{if eq .Package.Type 2}} + {{if .Metadata.Author}}
    {{svg "octicon-person" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.author"}}: {{.Metadata.Author}}
    {{end}} + {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} + {{if .Metadata.License}}
    {{svg "octicon-law" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.license"}}: {{.Metadata.License}}
    {{end}} +{{end}} \ No newline at end of file diff --git a/templates/repo/packages/metadata/nuget.tmpl b/templates/repo/packages/metadata/nuget.tmpl new file mode 100644 index 0000000000000..557d0d3e0ca4d --- /dev/null +++ b/templates/repo/packages/metadata/nuget.tmpl @@ -0,0 +1,4 @@ +{{if eq .Package.Type 1}} + {{if .Metadata.Authors}}
    {{svg "octicon-person" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.author"}}: {{.Metadata.Authors}}
    {{end}} + {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} +{{end}} \ No newline at end of file diff --git a/templates/repo/packages/metadata/pypi.tmpl b/templates/repo/packages/metadata/pypi.tmpl new file mode 100644 index 0000000000000..281cdbfe58649 --- /dev/null +++ b/templates/repo/packages/metadata/pypi.tmpl @@ -0,0 +1,6 @@ +{{if eq .Package.Type 4}} + {{if .Metadata.Author}}
    {{svg "octicon-person" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.author"}}: {{.Metadata.Author}}
    {{end}} + {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} + {{if .Metadata.License}}
    {{svg "octicon-law" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.license"}}: {{.Metadata.License}}
    {{end}} + {{if .Metadata.RequiresPython}}
    {{.i18n.Tr "repo.packages.python.requires"}}: {{.Metadata.RequiresPython}}
    {{end}} +{{end}} \ No newline at end of file diff --git a/templates/repo/packages/view.tmpl b/templates/repo/packages/view.tmpl new file mode 100644 index 0000000000000..54534dfd6fc3d --- /dev/null +++ b/templates/repo/packages/view.tmpl @@ -0,0 +1,79 @@ +{{template "base/head" .}} +
    + {{template "repo/header" .}} +
    +
    +
    +
    +
    +

    {{.Package.Name}} ({{.Package.Version}})

    +
    + {{ $timeStr := TimeSinceUnix .Package.CreatedUnix $.Lang }} +
    {{.i18n.Tr "repo.packages.published_by" $timeStr .Package.Creator.HomeLink (.Package.Creator.GetDisplayName | Escape) | Safe}} {{svg "octicon-package"}} {{.Package.Type.String}}
    +
    +
    + +
    + {{template "repo/packages/content/generic" .}} + {{template "repo/packages/content/nuget" .}} + {{template "repo/packages/content/npm" .}} + {{template "repo/packages/content/maven" .}} + {{template "repo/packages/content/pypi" .}} +
    + +
    +
    + {{.i18n.Tr "repo.packages.details"}} +
    {{svg "octicon-calendar" 16 "mr-3"}} {{.Package.CreatedUnix.FormatDate}}
    + {{template "repo/packages/metadata/generic" .}} + {{template "repo/packages/metadata/nuget" .}} + {{template "repo/packages/metadata/npm" .}} + {{template "repo/packages/metadata/maven" .}} + {{template "repo/packages/metadata/pypi" .}} + +
    + {{.i18n.Tr "repo.packages.assets"}} + {{range .Files}} + + {{end}} + + {{if .OtherVersions}} +
    + {{.i18n.Tr "repo.packages.versions"}} + {{range .OtherVersions}} +
    {{.Version}} {{$.i18n.Tr "repo.packages.versions.on"}} {{.CreatedUnix.FormatDate}}
    + {{end}} + {{end}} + + {{if .CanWritePackages}} +
    +
    + +
    + + {{end}} +
    +
    +
    +
    +
    +
    +{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index eb76a3b720064..f1356a52ce6b2 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -374,6 +374,19 @@
    + {{$isPackagesEnabled := .Repository.UnitEnabled $.UnitTypePackages}} +
    + + {{if .UnitTypePackages.UnitGlobalDisabled}} +
    + {{else}} +
    + {{end}} + + +
    +
    + {{$isProjectsEnabled := .Repository.UnitEnabled $.UnitTypeProjects}}
    From 8c6715b03558186a662241366adf9263f46f0446 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 20 Jul 2021 22:18:24 +0000 Subject: [PATCH 015/130] Changed method name. --- routers/api/v1/api.go | 8 ++++---- routers/api/v1/packages/generic/generic.go | 4 ++-- routers/api/v1/packages/npm/npm.go | 4 ++-- routers/api/v1/packages/nuget/nuget.go | 4 ++-- routers/api/v1/packages/pypi/pypi.go | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index f9de2ed23ee9f..0f3e62e6fc2df 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -979,7 +979,7 @@ func Routes() *web.Route { m.Group("/packages", func() { m.Group("/generic", func() { m.Group("/{packagename}/{packageversion}/{filename}", func() { - m.Get("", generic.DownloadPackageContent) + m.Get("", generic.DownloadPackageFile) m.Put("", reqRepoWriter(models.UnitTypePackages), generic.UploadPackage) m.Delete("", reqRepoWriter(models.UnitTypePackages), generic.DeletePackage) }) @@ -1000,7 +1000,7 @@ func Routes() *web.Route { m.Get("/index.json", nuget.EnumeratePackageVersions) m.Group("/{version}", func() { m.Delete("/", reqRepoWriter(models.UnitTypePackages), nuget.DeletePackage) - m.Get("/{filename}", nuget.DownloadPackageContent) + m.Get("/{filename}", nuget.DownloadPackageFile) }) }) }, reqBasicAuth()) @@ -1008,12 +1008,12 @@ func Routes() *web.Route { m.Group("/{id}", func() { m.Get("", npm.PackageMetadata) m.Put("", reqRepoWriter(models.UnitTypePackages), npm.UploadPackage) - m.Get("/-/{version}/{filename}", npm.DownloadPackageContent) + m.Get("/-/{version}/{filename}", npm.DownloadPackageFile) }) }, reqToken()) m.Group("/pypi", func() { m.Post("/", reqRepoWriter(models.UnitTypePackages), pypi.UploadPackageFile) - m.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageContent) + m.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile) m.Get("/simple/{id}", pypi.PackageMetadata) }, reqBasicAuth()) }) diff --git a/routers/api/v1/packages/generic/generic.go b/routers/api/v1/packages/generic/generic.go index 0c742f0abf4c0..70763916a2fcc 100644 --- a/routers/api/v1/packages/generic/generic.go +++ b/routers/api/v1/packages/generic/generic.go @@ -22,8 +22,8 @@ import ( var packageNameRegex = regexp.MustCompile(`\A[A-Za-z0-9\.\_\-\+]+\z`) var filenameRegex = packageNameRegex -// DownloadPackageContent serves the specific generic package. -func DownloadPackageContent(ctx *context.APIContext) { +// DownloadPackageFile serves the specific generic package. +func DownloadPackageFile(ctx *context.APIContext) { packageName, packageVersion, filename, err := sanitizeParameters(ctx) if err != nil { ctx.Error(http.StatusBadRequest, "", err) diff --git a/routers/api/v1/packages/npm/npm.go b/routers/api/v1/packages/npm/npm.go index 75365943dc02d..5f5508fa9df8c 100644 --- a/routers/api/v1/packages/npm/npm.go +++ b/routers/api/v1/packages/npm/npm.go @@ -50,8 +50,8 @@ func PackageMetadata(ctx *context.APIContext) { ctx.JSON(http.StatusOK, resp) } -// DownloadPackageContent serves the content of a package -func DownloadPackageContent(ctx *context.APIContext) { +// DownloadPackageFile serves the content of a package +func DownloadPackageFile(ctx *context.APIContext) { packageName, err := url.QueryUnescape(ctx.Params("id")) if err != nil { ctx.Error(http.StatusBadRequest, "", err) diff --git a/routers/api/v1/packages/nuget/nuget.go b/routers/api/v1/packages/nuget/nuget.go index fa18f8e3380d4..85e9fe6db86d9 100644 --- a/routers/api/v1/packages/nuget/nuget.go +++ b/routers/api/v1/packages/nuget/nuget.go @@ -138,8 +138,8 @@ func EnumeratePackageVersions(ctx *context.APIContext) { ctx.JSON(http.StatusOK, resp) } -// DownloadPackageContent https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg -func DownloadPackageContent(ctx *context.APIContext) { +// DownloadPackageFile https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg +func DownloadPackageFile(ctx *context.APIContext) { packageName := ctx.Params("id") packageVersion := ctx.Params("version") filename := ctx.Params("filename") diff --git a/routers/api/v1/packages/pypi/pypi.go b/routers/api/v1/packages/pypi/pypi.go index 7c4e6e938804b..76888cd7ea665 100644 --- a/routers/api/v1/packages/pypi/pypi.go +++ b/routers/api/v1/packages/pypi/pypi.go @@ -56,8 +56,8 @@ func PackageMetadata(ctx *context.APIContext) { ctx.HTML(http.StatusOK, "api/packages/pypi/simple") } -// DownloadPackageContent serves the content of a package -func DownloadPackageContent(ctx *context.APIContext) { +// DownloadPackageFile serves the content of a package +func DownloadPackageFile(ctx *context.APIContext) { packageName := normalizer.Replace(ctx.Params("id")) packageVersion := ctx.Params("version") filename := ctx.Params("filename") From 4a57e223a1c50813a1761580da806f35faa1110b Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 20 Jul 2021 22:26:52 +0000 Subject: [PATCH 016/130] Added missing migration file. --- models/migrations/v189.go | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 models/migrations/v189.go diff --git a/models/migrations/v189.go b/models/migrations/v189.go new file mode 100644 index 0000000000000..56ee43c6942f5 --- /dev/null +++ b/models/migrations/v189.go @@ -0,0 +1,48 @@ +// Copyright 2021 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 migrations + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func addPackageTables(x *xorm.Engine) error { + type Package struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + CreatorID int64 + Type int `xorm:"UNIQUE(s) INDEX NOT NULL"` + Name string + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + Version string `xorm:"UNIQUE(s) INDEX NOT NULL"` + MetadataRaw string `xorm:"TEXT"` + + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` + } + + if err := x.Sync2(new(Package)); err != nil { + return err + } + + type PackageFile struct { + ID int64 `xorm:"pk autoincr"` + PackageID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + Size int64 + Name string + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + HashMD5 string `xorm:"hash_md5"` + HashSHA1 string `xorm:"hash_sha1"` + HashSHA256 string `xorm:"hash_sha256"` + HashSHA512 string `xorm:"hash_sha512"` + + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` + } + + return x.Sync2(new(PackageFile)) +} From b8ac3e3ad94006bb3c8989c54c45fe54eeeec044 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 21 Jul 2021 07:16:00 +0200 Subject: [PATCH 017/130] Set page info. --- routers/web/repo/packages.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index d0168193e6a7f..a3cef8f8b68ff 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -37,6 +37,7 @@ func MustEnablePackages(ctx *context.Context) { // Packages displays a list of all packages in the repository func Packages(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.packages") + ctx.Data["IsPackagesPage"] = true query := ctx.QueryTrim("q") packageType := ctx.QueryTrim("package_type") @@ -66,7 +67,6 @@ func Packages(ctx *context.Context) { } ctx.Data["Packages"] = packages - ctx.Data["IsPackagesPage"] = true ctx.Data["Query"] = query ctx.Data["PackageType"] = packageType @@ -97,6 +97,9 @@ func ViewPackage(ctx *context.Context) { ctx.ServerError("LoadCreator", err) return } + + ctx.Data["Title"] = p.Name + ctx.Data["IsPackagesPage"] = true ctx.Data["Package"] = p var metadata interface{} From 1386b27f7a29e756bdec29d2155c59a912c40087 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 21 Jul 2021 21:35:09 +0000 Subject: [PATCH 018/130] Added documentation. --- docs/content/doc/developers.en-us.md | 2 +- docs/content/doc/developers.zh-tw.md | 2 +- docs/content/doc/features/comparison.en-us.md | 3 +- docs/content/doc/packages.en-us.md | 12 ++ docs/content/doc/packages/generic.en-us.md | 82 ++++++++++++++ docs/content/doc/packages/maven.en-us.md | 103 ++++++++++++++++++ docs/content/doc/packages/npm.en-us.md | 81 ++++++++++++++ docs/content/doc/packages/nuget.en-us.md | 90 +++++++++++++++ docs/content/doc/packages/pypi.en-us.md | 80 ++++++++++++++ docs/content/doc/translation.de-de.md | 2 +- docs/content/doc/translation.en-us.md | 2 +- docs/content/doc/translation.zh-tw.md | 2 +- 12 files changed, 455 insertions(+), 6 deletions(-) create mode 100644 docs/content/doc/packages.en-us.md create mode 100644 docs/content/doc/packages/generic.en-us.md create mode 100644 docs/content/doc/packages/maven.en-us.md create mode 100644 docs/content/doc/packages/npm.en-us.md create mode 100644 docs/content/doc/packages/nuget.en-us.md create mode 100644 docs/content/doc/packages/pypi.en-us.md diff --git a/docs/content/doc/developers.en-us.md b/docs/content/doc/developers.en-us.md index c24a23dfae4a6..917049e5dfa1e 100644 --- a/docs/content/doc/developers.en-us.md +++ b/docs/content/doc/developers.en-us.md @@ -8,6 +8,6 @@ draft: false menu: sidebar: name: "Developers" - weight: 50 + weight: 55 identifier: "developers" --- diff --git a/docs/content/doc/developers.zh-tw.md b/docs/content/doc/developers.zh-tw.md index e2fbd4a34f51d..c9ce6634ada83 100644 --- a/docs/content/doc/developers.zh-tw.md +++ b/docs/content/doc/developers.zh-tw.md @@ -8,6 +8,6 @@ draft: false menu: sidebar: name: "開發人員" - weight: 50 + weight: 55 identifier: "developers" --- diff --git a/docs/content/doc/features/comparison.en-us.md b/docs/content/doc/features/comparison.en-us.md index a703766855f4b..a6f9f66250599 100644 --- a/docs/content/doc/features/comparison.en-us.md +++ b/docs/content/doc/features/comparison.en-us.md @@ -48,7 +48,8 @@ _Symbols used in table:_ | Integrated Git-powered wiki | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (cloud only) | ✘ | | Deploy Tokens | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Repository Tokens with write rights | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Built-in Container Registry | [✘](https://github.com/go-gitea/gitea/issues/2316) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| Built-in Container Registry | [✘](https://github.com/go-gitea/gitea/issues/2316) | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Built-in Package Registry | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | | External git mirroring | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ | | FIDO U2F (2FA) | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | | Built-in CI/CD | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | diff --git a/docs/content/doc/packages.en-us.md b/docs/content/doc/packages.en-us.md new file mode 100644 index 0000000000000..e613b6b250e27 --- /dev/null +++ b/docs/content/doc/packages.en-us.md @@ -0,0 +1,12 @@ +--- +date: "2021-07-20T00:00:00+00:00" +title: "Package Registry" +slug: "packages" +toc: false +draft: false +menu: + sidebar: + name: "Package Registry" + weight: 45 + identifier: "packages" +--- diff --git a/docs/content/doc/packages/generic.en-us.md b/docs/content/doc/packages/generic.en-us.md new file mode 100644 index 0000000000000..cb40c5e5b00e3 --- /dev/null +++ b/docs/content/doc/packages/generic.en-us.md @@ -0,0 +1,82 @@ +--- +date: "2021-07-20T00:00:00+00:00" +title: "Generic Packages Repository" +slug: "generic" +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "Generic" + weight: 10 + identifier: "generic" +--- + +# Generic Packages Repository + +Publish generic files, like release binaries or other output, in your project’s Package Registry. + +**Table of Contents** + +{{< toc >}} + +## Authenticate to the package registry + +To authenticate to the Package Registry, you need to provide [custom HTTP headers or use HTTP Basic authentication]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}). + +## Publish a package + +To publish a generic 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. + +``` +PUT https://gitea.example.com/api/v1/repos/{owner}/{repository}/packages/generic/{package_name}/{package_version}/{file_name} +``` + +| Parameter | Description | +| ----------------- | ----------- | +| `owner` | The owner of the repository. | +| `repository` | The name of the repository. | +| `package_name` | The package name. It can contain only lowercase letters (`a-z`), uppercase letter (`A-Z`), numbers (`0-9`), dots (`.`), hyphens (`-`), or underscores (`_`). | +| `package_version` | The package version as described in the [SemVer](https://semver.org/) spec. | +| `file_name` | The filename. It can contain only lowercase letters (`a-z`), uppercase letter (`A-Z`), numbers (`0-9`), dots (`.`), hyphens (`-`), or underscores (`_`). | + +Example request using HTTP Basic authentication: + +```shell +curl --user your_username:your_password_or_token \ + --upload-file path/to/file.bin \ + "https://gitea.example.com/api/v1/repos/testuser/test-repository/packages/generic/test_package/1.0.0/file.bin" +``` + +The server reponds with the following HTTP Status codes. + +| HTTP Status Code | Meaning | +| ----------------- | ------- | +| `201 Created` | The package has been published. | +| `400 Bad Request` | The package name and/or version are invalid or a package with the same name and version already exist. | + +## Download a package + +To download a generic package perform a HTTP GET operation. + +``` +GET https://gitea.example.com/api/v1/repos/{owner}/{repository}/packages/generic/{package_name}/{package_version}/{file_name} +``` + +| Parameter | Description | +| ----------------- | ----------- | +| `owner` | The owner of the repository. | +| `repository` | The name of the repository. | +| `package_name` | The package name. | +| `package_version` | The package version. | +| `file_name` | The filename. | + +The file content is served in the response body. The response content type is `application/octet-stream`. + +Example request using HTTP Basic authentication: + +```shell +curl --user your_username:your_token_or_password \ + "https://gitea.example.com/api/v1/repos/testuser/test-repository/packages/generic/test_package/1.0.0/file.bin" +``` diff --git a/docs/content/doc/packages/maven.en-us.md b/docs/content/doc/packages/maven.en-us.md new file mode 100644 index 0000000000000..173acb16194a9 --- /dev/null +++ b/docs/content/doc/packages/maven.en-us.md @@ -0,0 +1,103 @@ +--- +date: "2021-07-20T00:00:00+00:00" +title: "Maven Packages Repository" +slug: "maven" +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "Maven" + weight: 40 + identifier: "maven" +--- + +# Maven Packages Repository + +Publish [Maven](https://maven.apache.org) packages in your project’s Package Registry. + +**Table of Contents** + +{{< toc >}} + +## Requirements + +To work with the Maven package registry, you can use [Maven](https://maven.apache.org/install.html) or [Gradle](https://gradle.org/install/). +The following examples use `Maven`. + +## Configuring the package registry + +To register the project’s package registry you first need to add your access token to the [`settings.xml`](https://maven.apache.org/settings.html) file: + +```xml + + + + gitea + + + + Authorization + token {access_token} + + + + + + +``` + +Afterwards add the following sections to your project `pom.xml` file: + +```xml + + + gitea + https://gitea.example.com/api/v1/repos/{owner}/{repository}/packages/maven + + + + + gitea + https://gitea.example.com/api/v1/repos/{owner}/{repository}/packages/maven + + + gitea + https://gitea.example.com/api/v1/repos/{owner}/{repository}/packages/maven + + +``` + +| Parameter | Description | +| -------------- | ----------- | +| `access_token` | Your [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}). | +| `owner` | The owner of the repository. | +| `repository` | The name of the repository. | + +## Publish a package + +To publish a package simply run: + +```shell +mvn deploy +``` + +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 Maven package from the package registry, add a new dependency to your project `pom.xml` file: + +```xml + + com.test.package + test_project + 1.0.0 + +``` + +Afterwards run: + +```shell +mvn install +``` \ No newline at end of file diff --git a/docs/content/doc/packages/npm.en-us.md b/docs/content/doc/packages/npm.en-us.md new file mode 100644 index 0000000000000..3a823a47c0b7e --- /dev/null +++ b/docs/content/doc/packages/npm.en-us.md @@ -0,0 +1,81 @@ +--- +date: "2021-07-20T00:00:00+00:00" +title: "npm Packages Repository" +slug: "npm" +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "npm" + weight: 30 + identifier: "npm" +--- + +# npm Packages Repository + +Publish [npm](https://www.npmjs.com/) packages in your project’s Package Registry. + +**Table of Contents** + +{{< toc >}} + +## Requirements + +To work with the npm package registry, you need [Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm/) or an other tool like [Yarn](https://classic.yarnpkg.com/en/docs/install). + +Only [scoped](https://docs.npmjs.com/misc/scope/) packages are supported. + +The following examples use the `npm` tool with the scope `@test`. + +## Configuring the package registry + +To register the project’s package registry you need to configure a new package source. + +```shell +npm config set {scope}:registry https://gitea.example.com/api/v1/repos/{owner}/{repository}/packages/npm +npm config set -- '//gitea.example.com/api/v1/repos/{owner}/{repository}/packages/npm/:_authToken' "{token}" +``` + +| Parameter | Description | +| ------------ | ----------- | +| `scope` | The scope of the packages. | +| `owner` | The owner of the repository. | +| `repository` | The name of the repository. | +| `token` | Your [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}). | + +For example: + +```shell +npm config set @test:registry https://gitea.example.com/api/v1/repos/testuser/test-repository/packages/npm +npm config set -- '//gitea.example.com/api/v1/repos/testuser/test-repository/packages/npm/:_authToken' "personal_access_token" +``` + +## Publish a package + +Publish a package by running the following command in your project: + +```shell +npm publish +``` + +You cannot publish a package if a package of the same name and version already exists. You must delete the existing package first. + +## Install a package + +To install a package from the package registry, execute the following command: + +```shell +npm install {scope}/{package_name} +``` + +| Parameter | Description | +| -------------- | ----------- | +| `scope` | The scope of the packages. | +| `package_name` | The package name. | + +For example: + +```shell +npm install @test/test_package +``` \ No newline at end of file diff --git a/docs/content/doc/packages/nuget.en-us.md b/docs/content/doc/packages/nuget.en-us.md new file mode 100644 index 0000000000000..8aaef7d661908 --- /dev/null +++ b/docs/content/doc/packages/nuget.en-us.md @@ -0,0 +1,90 @@ +--- +date: "2021-07-20T00:00:00+00:00" +title: "NuGet Packages Repository" +slug: "nuget" +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "NuGet" + weight: 20 + identifier: "nuget" +--- + +# NuGet Packages Repository + +Publish [NuGet](https://www.nuget.org/) packages in your project’s Package Registry. + +**Table of Contents** + +{{< toc >}} + +## Requirements + +To work with the NuGet package registry, you can use command-line interface (CLI) tools as well as NuGet features in various IDEs like Visual Studio. +More informations about NuGet clients can be found in [the official documentation](https://docs.microsoft.com/en-us/nuget/install-nuget-client-tools). +The following examples use the `dotnet nuget` tool. + +## Configuring the package registry + +To register the project’s package registry you need to configure a new NuGet feed source: + +```shell +dotnet nuget add source --name {source_name} --username {username} --password {password} https://gitea.example.com/api/v1/repos/{owner}/{repository}/packages/nuget/index.json +``` + +| Parameter | Description | +| ------------- | ----------- | +| `source_name` | The desired source name. | +| `username` | Your Gitea username. | +| `password` | Your Gitea password or a personal access token. | +| `owner` | The owner of the repository. | +| `repository` | The name of the repository. | + +For example: + +```shell +dotnet nuget add source --name gitea --username testuser --password password123 https://gitea.example.com/api/v1/repos/testuser/test-repository/packages/nuget/index.json +``` + +## Publish a package + +Publish a package by running the following command: + +```shell +dotnet nuget push --source {source_name} {package_file} +``` + +| Parameter | Description | +| -------------- | ----------- | +| `source_name` | The desired source name. | +| `package_file` | Path to the package `.nupkg` file. | + +For example: + +```shell +dotnet nuget push --source gitea test_package.1.0.0.nupkg +``` + +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 NuGet package from the package registry, execute the following command: + +```shell +dotnet add package --source {source_name} --version {package_version} {package_name} +``` + +| Parameter | Description | +| ----------------- | ----------- | +| `source_name` | The desired source name. | +| `package_name` | The package name. | +| `package_version` | The package version. | + +For example: + +```shell +dotnet add package --source gitea --version 1.0.0 test_package +``` \ No newline at end of file diff --git a/docs/content/doc/packages/pypi.en-us.md b/docs/content/doc/packages/pypi.en-us.md new file mode 100644 index 0000000000000..33372ee553c56 --- /dev/null +++ b/docs/content/doc/packages/pypi.en-us.md @@ -0,0 +1,80 @@ +--- +date: "2021-07-20T00:00:00+00:00" +title: "PyPI Packages Repository" +slug: "pypi" +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "PyPI" + weight: 50 + identifier: "pypi" +--- + +# PyPI Packages Repository + +Publish [PyPI](https://pypi.org/) packages in your project’s Package Registry. + +**Table of Contents** + +{{< toc >}} + +## Requirements + +To work with the PyPI package registry, you need to use the tools [pip](https://pypi.org/project/pip/) to consume and [twine](https://pypi.org/project/twine/) to publish packages. + +## Configuring the package registry + +To register the project’s package registry you need to edit your local `~/.pypirc` file. Add + +```ini +[distutils] +index-servers = gitea + +[gitea] +repository = https://gitea.example.com/api/v1/repos/{owner}/{repository}/packages/pypi +username = {username} +password = {password} +``` + +| Placeholder | Description | +| ------------ | ----------- | +| `owner` | The owner of the repository. | +| `repository` | The name of the repository. | +| `username` | Your Gitea username. | +| `password` | Your Gitea password or a [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}). | + +## Publish a package + +Publish a package by running the following command: + +```shell +python3 -m twine upload --repository gitea /path/to/files/* +``` + +The package files have the extensions `.tar.gz` and `.whl`. + +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 PyPI package from the package registry, execute the following command: + +```shell +pip install --index-url https://{username}:{password}@gitea.example.com/api/v1/repos/{owner}/{repository}/packages/pypi/simple --no-deps {package_name} +``` + +| Parameter | Description | +| ----------------- | ----------- | +| `username` | Your Gitea username. | +| `password` | Your Gitea password or a personal access token. | +| `owner` | The owner of the repository. | +| `repository` | The name of the repository. | +| `package_name` | The package name. | + +For example: + +```shell +pip install --index-url https://testuser:password123@gitea.example.com/api/v1/repos/testuser/test-repository/packages/pypi/simple --no-deps test_package +``` diff --git a/docs/content/doc/translation.de-de.md b/docs/content/doc/translation.de-de.md index 585783a706256..3470faa59b4c4 100644 --- a/docs/content/doc/translation.de-de.md +++ b/docs/content/doc/translation.de-de.md @@ -8,6 +8,6 @@ draft: false menu: sidebar: name: "Übersetzung" - weight: 45 + weight: 50 identifier: "translation" --- diff --git a/docs/content/doc/translation.en-us.md b/docs/content/doc/translation.en-us.md index 208eb32ab8aa4..c281088503aec 100644 --- a/docs/content/doc/translation.en-us.md +++ b/docs/content/doc/translation.en-us.md @@ -8,6 +8,6 @@ draft: false menu: sidebar: name: "Translation" - weight: 45 + weight: 50 identifier: "translation" --- diff --git a/docs/content/doc/translation.zh-tw.md b/docs/content/doc/translation.zh-tw.md index ca820c093c697..5374e87e891c8 100644 --- a/docs/content/doc/translation.zh-tw.md +++ b/docs/content/doc/translation.zh-tw.md @@ -8,6 +8,6 @@ draft: false menu: sidebar: name: "翻譯" - weight: 45 + weight: 50 identifier: "translation" --- From 8ba72d71aded9bedc4a61a39bde92fde19ecf257 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 21 Jul 2021 21:50:46 +0000 Subject: [PATCH 019/130] Added documentation links. --- options/locale/locale_en-US.ini | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index c4e76e28f00e2..38ee5636fe445 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1524,20 +1524,20 @@ packages.delete = Delete package packages.delete.notice = You are about to delete %s (%s). This operation is irreversible, are you sure? packages.delete.success = The package has been deleted. packages.generic.use = Download the package, for example with curl: -packages.generic.documentation = For more information on the generic registry, see the documentation. +packages.generic.documentation = For more information on the generic registry, see the documentation. packages.maven.registry = Setup this registry in your projects pom.xml file: packages.maven.use = To use the package include the following in the dependencies block in the pom.xml file: packages.maven.use_2 = Alternatively use this Maven command: -packages.maven.documentation = For more information on the Maven registry, see the documentation. +packages.maven.documentation = For more information on the Maven registry, see the documentation. packages.nuget.registry = Setup this registry with the following command: packages.nuget.use = Install the package with the following command: -packages.nuget.documentation = For more information on the NuGet registry, see the documentation. +packages.nuget.documentation = For more information on the NuGet registry, see the documentation. packages.npm.registry = Setup this registry in your projects .npmrc file: packages.npm.use = To install the package use the following command: -packages.npm.documentation = For more information on the npm registry, see the documentation. +packages.npm.documentation = For more information on the npm registry, see the documentation. packages.pypi.requires = Requires Python packages.pypi.use = To install the package use the following command: -packages.pypi.documentation = For more information on the PyPI registry, see the documentation. +packages.pypi.documentation = For more information on the PyPI registry, see the documentation. signing.will_sign = This commit will be signed with key '%s' signing.wont_sign.error = There was an error whilst checking if the commit could be signed From cab94bbafddc43e16f9e772b2b74437775081d9d Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Thu, 22 Jul 2021 15:38:18 +0000 Subject: [PATCH 020/130] Fixed wrong error message. --- models/package.go | 2 +- routers/web/repo/packages.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models/package.go b/models/package.go index c2de2e5c404c5..e4df42e77b091 100644 --- a/models/package.go +++ b/models/package.go @@ -112,7 +112,7 @@ func (p *Package) GetFileByName(name string) (*PackageFile, error) { return nil, err } if !has { - return nil, ErrDuplicatePackage + return nil, ErrPackageFileNotExist } return pf, nil } diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index a3cef8f8b68ff..2467527c1949e 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -97,7 +97,7 @@ func ViewPackage(ctx *context.Context) { ctx.ServerError("LoadCreator", err) return } - + ctx.Data["Title"] = p.Name ctx.Data["IsPackagesPage"] = true ctx.Data["Package"] = p From f16296c0202ced1a48b33eb8bcb9e6d257130335 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Thu, 22 Jul 2021 16:04:26 +0000 Subject: [PATCH 021/130] Lint template files. --- templates/api/packages/pypi/simple.tmpl | 2 +- templates/repo/packages/content/generic.tmpl | 2 +- templates/repo/packages/content/maven.tmpl | 2 +- templates/repo/packages/content/npm.tmpl | 2 +- templates/repo/packages/content/nuget.tmpl | 2 +- templates/repo/packages/content/pypi.tmpl | 2 +- templates/repo/packages/list.tmpl | 2 +- templates/repo/packages/metadata/maven.tmpl | 2 +- templates/repo/packages/metadata/npm.tmpl | 2 +- templates/repo/packages/metadata/nuget.tmpl | 2 +- templates/repo/packages/metadata/pypi.tmpl | 2 +- templates/repo/packages/view.tmpl | 9 +-------- 12 files changed, 12 insertions(+), 19 deletions(-) diff --git a/templates/api/packages/pypi/simple.tmpl b/templates/api/packages/pypi/simple.tmpl index f3ed056505179..3815b56e6f33b 100644 --- a/templates/api/packages/pypi/simple.tmpl +++ b/templates/api/packages/pypi/simple.tmpl @@ -12,4 +12,4 @@ {{end}} {{end}} - \ No newline at end of file + diff --git a/templates/repo/packages/content/generic.tmpl b/templates/repo/packages/content/generic.tmpl index 4c3969ff4799f..c3d398fb90db9 100644 --- a/templates/repo/packages/content/generic.tmpl +++ b/templates/repo/packages/content/generic.tmpl @@ -9,4 +9,4 @@
    {{.i18n.Tr "repo.packages.generic.documentation" | Safe}}
    -{{end}} \ No newline at end of file +{{end}} diff --git a/templates/repo/packages/content/maven.tmpl b/templates/repo/packages/content/maven.tmpl index 2ffced4813f73..fd558fabd6925 100644 --- a/templates/repo/packages/content/maven.tmpl +++ b/templates/repo/packages/content/maven.tmpl @@ -56,4 +56,4 @@ {{end}} {{end}} -{{end}} \ No newline at end of file +{{end}} diff --git a/templates/repo/packages/content/npm.tmpl b/templates/repo/packages/content/npm.tmpl index 8c0b5f66b2afa..5298bbca5a4f6 100644 --- a/templates/repo/packages/content/npm.tmpl +++ b/templates/repo/packages/content/npm.tmpl @@ -35,4 +35,4 @@ {{end}} {{end}} -{{end}} \ No newline at end of file +{{end}} diff --git a/templates/repo/packages/content/nuget.tmpl b/templates/repo/packages/content/nuget.tmpl index 57a41aeeb5d4a..291f94df0385d 100644 --- a/templates/repo/packages/content/nuget.tmpl +++ b/templates/repo/packages/content/nuget.tmpl @@ -33,4 +33,4 @@ {{end}} {{end}} -{{end}} \ No newline at end of file +{{end}} diff --git a/templates/repo/packages/content/pypi.tmpl b/templates/repo/packages/content/pypi.tmpl index dfbc6a8529e19..15d61d4b34492 100644 --- a/templates/repo/packages/content/pypi.tmpl +++ b/templates/repo/packages/content/pypi.tmpl @@ -21,4 +21,4 @@ {{end}} {{end}} -{{end}} \ No newline at end of file +{{end}} diff --git a/templates/repo/packages/list.tmpl b/templates/repo/packages/list.tmpl index cb342ab31ee8e..60338166afbd7 100644 --- a/templates/repo/packages/list.tmpl +++ b/templates/repo/packages/list.tmpl @@ -52,4 +52,4 @@ -{{template "base/footer" .}} \ No newline at end of file +{{template "base/footer" .}} diff --git a/templates/repo/packages/metadata/maven.tmpl b/templates/repo/packages/metadata/maven.tmpl index 0ea9adfd5b8d4..246d5889a5c19 100644 --- a/templates/repo/packages/metadata/maven.tmpl +++ b/templates/repo/packages/metadata/maven.tmpl @@ -7,4 +7,4 @@
    {{svg "octicon-law" 16 "mr-3"}} {{.}}
    {{end}} {{end}} -{{end}} \ No newline at end of file +{{end}} diff --git a/templates/repo/packages/metadata/npm.tmpl b/templates/repo/packages/metadata/npm.tmpl index 0c37cc968cad4..702fc94d6ed97 100644 --- a/templates/repo/packages/metadata/npm.tmpl +++ b/templates/repo/packages/metadata/npm.tmpl @@ -2,4 +2,4 @@ {{if .Metadata.Author}}
    {{svg "octicon-person" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.author"}}: {{.Metadata.Author}}
    {{end}} {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} {{if .Metadata.License}}
    {{svg "octicon-law" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.license"}}: {{.Metadata.License}}
    {{end}} -{{end}} \ No newline at end of file +{{end}} diff --git a/templates/repo/packages/metadata/nuget.tmpl b/templates/repo/packages/metadata/nuget.tmpl index 557d0d3e0ca4d..2ef835826a3ad 100644 --- a/templates/repo/packages/metadata/nuget.tmpl +++ b/templates/repo/packages/metadata/nuget.tmpl @@ -1,4 +1,4 @@ {{if eq .Package.Type 1}} {{if .Metadata.Authors}}
    {{svg "octicon-person" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.author"}}: {{.Metadata.Authors}}
    {{end}} {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} -{{end}} \ No newline at end of file +{{end}} diff --git a/templates/repo/packages/metadata/pypi.tmpl b/templates/repo/packages/metadata/pypi.tmpl index 281cdbfe58649..113b0c92bac92 100644 --- a/templates/repo/packages/metadata/pypi.tmpl +++ b/templates/repo/packages/metadata/pypi.tmpl @@ -3,4 +3,4 @@ {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} {{if .Metadata.License}}
    {{svg "octicon-law" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.license"}}: {{.Metadata.License}}
    {{end}} {{if .Metadata.RequiresPython}}
    {{.i18n.Tr "repo.packages.python.requires"}}: {{.Metadata.RequiresPython}}
    {{end}} -{{end}} \ No newline at end of file +{{end}} diff --git a/templates/repo/packages/view.tmpl b/templates/repo/packages/view.tmpl index 54534dfd6fc3d..72b6ad2f8810f 100644 --- a/templates/repo/packages/view.tmpl +++ b/templates/repo/packages/view.tmpl @@ -12,7 +12,6 @@
    {{.i18n.Tr "repo.packages.published_by" $timeStr .Package.Creator.HomeLink (.Package.Creator.GetDisplayName | Escape) | Safe}} {{svg "octicon-package"}} {{.Package.Type.String}}
    -
    {{template "repo/packages/content/generic" .}} {{template "repo/packages/content/nuget" .}} @@ -20,7 +19,6 @@ {{template "repo/packages/content/maven" .}} {{template "repo/packages/content/pypi" .}}
    -
    {{.i18n.Tr "repo.packages.details"}} @@ -30,13 +28,11 @@ {{template "repo/packages/metadata/npm" .}} {{template "repo/packages/metadata/maven" .}} {{template "repo/packages/metadata/pypi" .}} -
    {{.i18n.Tr "repo.packages.assets"}} {{range .Files}} {{end}} - {{if .OtherVersions}}
    {{.i18n.Tr "repo.packages.versions"}} @@ -44,7 +40,6 @@
    {{.Version}} {{$.i18n.Tr "repo.packages.versions.on"}} {{.CreatedUnix.FormatDate}}
    {{end}} {{end}} - {{if .CanWritePackages}}
    @@ -58,10 +53,8 @@
    {{.i18n.Tr "repo.packages.delete.notice" .Package.Name .Package.Version}}
    -
    {{.CsrfTokenHtml}} -
    {{.i18n.Tr "cancel"}}
    @@ -76,4 +69,4 @@
    -{{template "base/footer" .}} \ No newline at end of file +{{template "base/footer" .}} From 814a942cd47d425525b8a99d8fa649d5ba687be6 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sat, 7 Aug 2021 10:49:42 +0000 Subject: [PATCH 022/130] Fixed merge errors. --- routers/api/v1/packages/nuget/nuget.go | 6 +++--- routers/web/repo/packages.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/routers/api/v1/packages/nuget/nuget.go b/routers/api/v1/packages/nuget/nuget.go index 85e9fe6db86d9..29902a67fb1b2 100644 --- a/routers/api/v1/packages/nuget/nuget.go +++ b/routers/api/v1/packages/nuget/nuget.go @@ -31,9 +31,9 @@ func ServiceIndex(ctx *context.APIContext) { // SearchService https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages func SearchService(ctx *context.APIContext) { - query := ctx.QueryTrim("q") - skip := ctx.QueryInt("skip") - take := ctx.QueryInt("take") + query := ctx.FormTrim("q") + skip := ctx.FormInt("skip") + take := ctx.FormInt("take") total, packages, err := models.SearchPackages(ctx.Repo.Repository.ID, models.PackageNuGet, query, skip, take) if err != nil { diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index 2467527c1949e..f84e101f2700a 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -39,9 +39,9 @@ func Packages(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.packages") ctx.Data["IsPackagesPage"] = true - query := ctx.QueryTrim("q") - packageType := ctx.QueryTrim("package_type") - page := ctx.QueryInt("page") + query := ctx.FormTrim("q") + packageType := ctx.FormTrim("package_type") + page := ctx.FormInt("page") if page <= 1 { page = 1 } From 94177549648b6e6d5a36735ad1b0950b3e880b4b Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sat, 7 Aug 2021 11:27:57 +0000 Subject: [PATCH 023/130] Fixed unit test storage path. --- models/unit_tests.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/models/unit_tests.go b/models/unit_tests.go index f8d6819333611..ac65a23de4cb2 100644 --- a/models/unit_tests.go +++ b/models/unit_tests.go @@ -76,6 +76,8 @@ func MainTest(m *testing.M, pathToGiteaRoot string) { setting.RepoArchive.Storage.Path = filepath.Join(setting.AppDataPath, "repo-archive") + setting.Packages.Storage.Path = filepath.Join(setting.AppDataPath, "packages") + if err = storage.Init(); err != nil { fatalTestError("storage.Init: %v\n", err) } From 02708f88bdc4c8a47ef22a64df1e2e1e6beaace4 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sun, 8 Aug 2021 18:45:39 +0000 Subject: [PATCH 024/130] Switch to json module. --- integrations/api_packages_maven_test.go | 6 +++--- modules/packages/npm/creator.go | 4 ++-- modules/packages/npm/creator_test.go | 19 ++++++++++--------- routers/api/v1/packages/maven/maven.go | 5 ++--- routers/api/v1/packages/maven/package.go | 5 ++--- routers/api/v1/packages/npm/package.go | 4 ++-- routers/api/v1/packages/nuget/package.go | 4 ++-- routers/api/v1/packages/pypi/package.go | 5 ++--- routers/web/repo/packages.go | 5 ++--- services/packages/packages.go | 5 ++--- 10 files changed, 29 insertions(+), 33 deletions(-) diff --git a/integrations/api_packages_maven_test.go b/integrations/api_packages_maven_test.go index d01e9234200c5..0d39428f25ed3 100644 --- a/integrations/api_packages_maven_test.go +++ b/integrations/api_packages_maven_test.go @@ -11,9 +11,9 @@ import ( "testing" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/packages/maven" - jsoniter "github.com/json-iterator/go" "github.com/stretchr/testify/assert" ) @@ -89,7 +89,7 @@ func TestPackageMaven(t *testing.T) { assert.Len(t, ps, 1) var m *maven.Metadata - err = jsoniter.Unmarshal([]byte(ps[0].MetadataRaw), &m) + err = json.Unmarshal([]byte(ps[0].MetadataRaw), &m) assert.NoError(t, err) assert.Empty(t, m.Description) @@ -99,7 +99,7 @@ func TestPackageMaven(t *testing.T) { assert.NoError(t, err) assert.Len(t, ps, 1) - err = jsoniter.Unmarshal([]byte(ps[0].MetadataRaw), &m) + err = json.Unmarshal([]byte(ps[0].MetadataRaw), &m) assert.NoError(t, err) assert.Equal(t, packageDescription, m.Description) }) diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index 33b99dc87bf2d..5639169ac8a43 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -16,10 +16,10 @@ import ( "strings" "time" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/validation" "github.com/hashicorp/go-version" - jsoniter "github.com/json-iterator/go" ) var ( @@ -122,7 +122,7 @@ type packageUpload struct { // ParsePackage parses the content into a NPM package func ParsePackage(r io.Reader) (*Package, error) { var upload packageUpload - if err := jsoniter.NewDecoder(r).Decode(&upload); err != nil { + if err := json.NewDecoder(r).Decode(&upload); err != nil { return nil, err } diff --git a/modules/packages/npm/creator_test.go b/modules/packages/npm/creator_test.go index c294b695adaf0..04946cf310641 100644 --- a/modules/packages/npm/creator_test.go +++ b/modules/packages/npm/creator_test.go @@ -11,7 +11,8 @@ import ( "strings" "testing" - jsoniter "github.com/json-iterator/go" + "code.gitea.io/gitea/modules/json" + "github.com/stretchr/testify/assert" ) @@ -30,7 +31,7 @@ func TestParsePackage(t *testing.T) { }) t.Run("InvalidUploadNoData", func(t *testing.T) { - b, _ := jsoniter.Marshal(packageUpload{}) + b, _ := json.Marshal(packageUpload{}) p, err := ParsePackage(bytes.NewReader(b)) assert.Nil(t, p) assert.ErrorIs(t, err, ErrInvalidPackage) @@ -38,7 +39,7 @@ func TestParsePackage(t *testing.T) { t.Run("InvalidPackageName", func(t *testing.T) { test := func(t *testing.T, name string) { - b, _ := jsoniter.Marshal(packageUpload{ + b, _ := json.Marshal(packageUpload{ PackageMetadata: PackageMetadata{ ID: name, Name: name, @@ -63,7 +64,7 @@ func TestParsePackage(t *testing.T) { t.Run("InvalidPackageVersion", func(t *testing.T) { version := "first-version" - b, _ := jsoniter.Marshal(packageUpload{ + b, _ := json.Marshal(packageUpload{ PackageMetadata: PackageMetadata{ ID: packageName, Name: packageName, @@ -81,7 +82,7 @@ func TestParsePackage(t *testing.T) { }) t.Run("InvalidAttachment", func(t *testing.T) { - b, _ := jsoniter.Marshal(packageUpload{ + b, _ := json.Marshal(packageUpload{ PackageMetadata: PackageMetadata{ ID: packageName, Name: packageName, @@ -103,7 +104,7 @@ func TestParsePackage(t *testing.T) { t.Run("InvalidData", func(t *testing.T) { filename := fmt.Sprintf("%s-%s.tgz", packageName, packageVersion) - b, _ := jsoniter.Marshal(packageUpload{ + b, _ := json.Marshal(packageUpload{ PackageMetadata: PackageMetadata{ ID: packageName, Name: packageName, @@ -127,7 +128,7 @@ func TestParsePackage(t *testing.T) { t.Run("InvalidIntegrity", func(t *testing.T) { filename := fmt.Sprintf("%s-%s.tgz", packageName, packageVersion) - b, _ := jsoniter.Marshal(packageUpload{ + b, _ := json.Marshal(packageUpload{ PackageMetadata: PackageMetadata{ ID: packageName, Name: packageName, @@ -154,7 +155,7 @@ func TestParsePackage(t *testing.T) { t.Run("InvalidIntegrity2", func(t *testing.T) { filename := fmt.Sprintf("%s-%s.tgz", packageName, packageVersion) - b, _ := jsoniter.Marshal(packageUpload{ + b, _ := json.Marshal(packageUpload{ PackageMetadata: PackageMetadata{ ID: packageName, Name: packageName, @@ -181,7 +182,7 @@ func TestParsePackage(t *testing.T) { t.Run("Valid", func(t *testing.T) { filename := fmt.Sprintf("%s-%s.tgz", packageName, packageVersion) - b, _ := jsoniter.Marshal(packageUpload{ + b, _ := json.Marshal(packageUpload{ PackageMetadata: PackageMetadata{ ID: packageName, Name: packageName, diff --git a/routers/api/v1/packages/maven/maven.go b/routers/api/v1/packages/maven/maven.go index 8b46f929ba83c..c47b7700f3b30 100644 --- a/routers/api/v1/packages/maven/maven.go +++ b/routers/api/v1/packages/maven/maven.go @@ -19,14 +19,13 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/packages" maven_module "code.gitea.io/gitea/modules/packages/maven" "code.gitea.io/gitea/modules/util/filebuffer" package_service "code.gitea.io/gitea/services/packages" - - jsoniter "github.com/json-iterator/go" ) const mavenMetadataFile = "maven-metadata.xml" @@ -217,7 +216,7 @@ func UploadPackageFile(ctx *context.APIContext) { log.Error("Error parsing package metadata: %v", err) } if metadata != nil { - raw, err := jsoniter.Marshal(metadata) + raw, err := json.Marshal(metadata) if err != nil { ctx.Error(http.StatusInternalServerError, "", err) return diff --git a/routers/api/v1/packages/maven/package.go b/routers/api/v1/packages/maven/package.go index 9ef5d0550d622..6ac7000bd33cb 100644 --- a/routers/api/v1/packages/maven/package.go +++ b/routers/api/v1/packages/maven/package.go @@ -8,9 +8,8 @@ import ( "sort" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/json" maven_module "code.gitea.io/gitea/modules/packages/maven" - - jsoniter "github.com/json-iterator/go" ) // Package represents a package with Maven metadata @@ -33,7 +32,7 @@ func intializePackages(packages []*models.Package) ([]*Package, error) { func intializePackage(p *models.Package) (*Package, error) { var m *maven_module.Metadata - err := jsoniter.Unmarshal([]byte(p.MetadataRaw), &m) + err := json.Unmarshal([]byte(p.MetadataRaw), &m) if err != nil { return nil, err } diff --git a/routers/api/v1/packages/npm/package.go b/routers/api/v1/packages/npm/package.go index b74f368cea256..ad46d04576ba2 100644 --- a/routers/api/v1/packages/npm/package.go +++ b/routers/api/v1/packages/npm/package.go @@ -8,10 +8,10 @@ import ( "sort" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/json" npm_module "code.gitea.io/gitea/modules/packages/npm" "github.com/hashicorp/go-version" - jsoniter "github.com/json-iterator/go" ) // Package represents a package with NPM metadata @@ -41,7 +41,7 @@ func intializePackage(p *models.Package) (*Package, error) { } var m *npm_module.Metadata - err = jsoniter.Unmarshal([]byte(p.MetadataRaw), &m) + err = json.Unmarshal([]byte(p.MetadataRaw), &m) if err != nil { return nil, err } diff --git a/routers/api/v1/packages/nuget/package.go b/routers/api/v1/packages/nuget/package.go index c4be91e77aa3b..94332fc9fc803 100644 --- a/routers/api/v1/packages/nuget/package.go +++ b/routers/api/v1/packages/nuget/package.go @@ -8,10 +8,10 @@ import ( "sort" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/json" nuget_module "code.gitea.io/gitea/modules/packages/nuget" "github.com/hashicorp/go-version" - jsoniter "github.com/json-iterator/go" ) // Package represents a package with NuGet metadata @@ -40,7 +40,7 @@ func intializePackage(p *models.Package) (*Package, error) { } var m *nuget_module.Metadata - err = jsoniter.Unmarshal([]byte(p.MetadataRaw), &m) + err = json.Unmarshal([]byte(p.MetadataRaw), &m) if err != nil { return nil, err } diff --git a/routers/api/v1/packages/pypi/package.go b/routers/api/v1/packages/pypi/package.go index 8050d4f94d5b7..ae9692314608d 100644 --- a/routers/api/v1/packages/pypi/package.go +++ b/routers/api/v1/packages/pypi/package.go @@ -6,9 +6,8 @@ package pypi import ( "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/json" pypi_module "code.gitea.io/gitea/modules/packages/pypi" - - jsoniter "github.com/json-iterator/go" ) // Package represents a package with NPM metadata @@ -32,7 +31,7 @@ func intializePackages(packages []*models.Package) ([]*Package, error) { func intializePackage(p *models.Package) (*Package, error) { var m *pypi_module.Metadata - err := jsoniter.Unmarshal([]byte(p.MetadataRaw), &m) + err := json.Unmarshal([]byte(p.MetadataRaw), &m) if err != nil { return nil, err } diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index f84e101f2700a..4604c256031f1 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/packages/maven" "code.gitea.io/gitea/modules/packages/npm" "code.gitea.io/gitea/modules/packages/nuget" @@ -18,8 +19,6 @@ import ( "code.gitea.io/gitea/modules/setting" package_service "code.gitea.io/gitea/services/packages" - - jsoniter "github.com/json-iterator/go" ) const ( @@ -114,7 +113,7 @@ func ViewPackage(ctx *context.Context) { metadata = &pypi.Metadata{} } if metadata != nil { - if err := jsoniter.Unmarshal([]byte(p.MetadataRaw), &metadata); err != nil { + if err := json.Unmarshal([]byte(p.MetadataRaw), &metadata); err != nil { ctx.ServerError("Unmarshal", err) return } diff --git a/services/packages/packages.go b/services/packages/packages.go index 8550d58ca27db..8cfdbc1ad663e 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -14,15 +14,14 @@ import ( "strings" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" - - jsoniter "github.com/json-iterator/go" ) // CreatePackage creates a new package func CreatePackage(creator *models.User, repository *models.Repository, packageType models.PackageType, name, version string, metadata interface{}, allowDuplicate bool) (*models.Package, error) { - metadataJSON, err := jsoniter.Marshal(metadata) + metadataJSON, err := json.Marshal(metadata) if err != nil { log.Error("Error converting metadata to JSON: %v", err) return nil, err From cb360c03c5650c76697f5b331962135d54a85026 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 9 Aug 2021 16:06:45 +0000 Subject: [PATCH 025/130] Added suggestions. --- models/package.go | 7 ++--- templates/repo/packages/content/maven.tmpl | 8 +++--- templates/repo/packages/content/npm.tmpl | 8 +++--- templates/repo/packages/content/nuget.tmpl | 10 ++++--- templates/repo/packages/metadata/maven.tmpl | 8 +++--- templates/repo/packages/metadata/npm.tmpl | 6 ++--- templates/repo/packages/metadata/nuget.tmpl | 4 +-- templates/repo/packages/metadata/pypi.tmpl | 8 +++--- templates/repo/packages/view.tmpl | 29 +++++++++++++++------ 9 files changed, 54 insertions(+), 34 deletions(-) diff --git a/models/package.go b/models/package.go index e4df42e77b091..d14216b3e4def 100644 --- a/models/package.go +++ b/models/package.go @@ -270,9 +270,10 @@ func GetPackageByNameAndVersion(repositoryID int64, packageType PackageType, pac // SearchPackages searches for packages by name and can be used to navigate through the package list func SearchPackages(repositoryID int64, packageType PackageType, query string, skip, take int) (int64, []*Package, error) { - cond := builder.NewCond() - cond = cond.And(builder.Eq{"repo_id": repositoryID}) - cond = cond.And(builder.Eq{"type": packageType}) + var cond builder.Cond = builder.Eq{ + "repo_id": repositoryID, + "type": packageType, + } if query != "" { cond = cond.And(builder.Like{"lower_name", strings.ToLower(query)}) } diff --git a/templates/repo/packages/content/maven.tmpl b/templates/repo/packages/content/maven.tmpl index fd558fabd6925..0a81641ac48bb 100644 --- a/templates/repo/packages/content/maven.tmpl +++ b/templates/repo/packages/content/maven.tmpl @@ -51,9 +51,11 @@ {{if .Metadata.Dependencies}}

    {{.i18n.Tr "repo.packages.dependencies"}}

    - {{range .Metadata.Dependencies}} -
    {{.GroupID}}:{{.ArtifactID}} ({{.Version}})
    - {{end}} +
    + {{range .Metadata.Dependencies}} +
    {{.GroupID}}:{{.ArtifactID}} ({{.Version}})
    + {{end}} +
    {{end}} {{end}} diff --git a/templates/repo/packages/content/npm.tmpl b/templates/repo/packages/content/npm.tmpl index 5298bbca5a4f6..a756fa2b19e55 100644 --- a/templates/repo/packages/content/npm.tmpl +++ b/templates/repo/packages/content/npm.tmpl @@ -30,9 +30,11 @@ {{if .Metadata.Dependencies}}

    {{.i18n.Tr "repo.packages.dependencies"}}

    - {{range $dependency, $version := .Metadata.Dependencies}} -
    {{$dependency}} ({{$version}})
    - {{end}} +
    + {{range $dependency, $version := .Metadata.Dependencies}} +
    {{$dependency}} ({{$version}})
    + {{end}} +
    {{end}} {{end}} diff --git a/templates/repo/packages/content/nuget.tmpl b/templates/repo/packages/content/nuget.tmpl index 291f94df0385d..e4d931402149e 100644 --- a/templates/repo/packages/content/nuget.tmpl +++ b/templates/repo/packages/content/nuget.tmpl @@ -26,10 +26,12 @@

    {{.i18n.Tr "repo.packages.dependencies"}}

    {{range $framework, $dependencies := .Metadata.Dependencies}} -
    {{$framework}}
    - {{range $dependencies}} -
    {{.ID}} ({{.Version}})
    - {{end}} +
    +
    {{$framework}}
    + {{range $dependencies}} +
    {{.ID}} ({{.Version}})
    + {{end}} +
    {{end}}
    {{end}} diff --git a/templates/repo/packages/metadata/maven.tmpl b/templates/repo/packages/metadata/maven.tmpl index 246d5889a5c19..b7015be23c34e 100644 --- a/templates/repo/packages/metadata/maven.tmpl +++ b/templates/repo/packages/metadata/maven.tmpl @@ -1,10 +1,10 @@ {{if eq .Package.Type 3}} - {{if .Metadata.Name}}
    {{svg "octicon-note" 16 "mr-3"}} {{.Metadata.Name}}
    {{end}} - {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} + {{if .Metadata.Name}}
    {{svg "octicon-note" 16 "mr-3"}} {{.Metadata.Name}}
    {{end}} + {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} {{if .Metadata.Licenses}} -
    {{.i18n.Tr "repo.packages.details.license"}}:
    +
    {{.i18n.Tr "repo.packages.details.license"}}:
    {{range .Metadata.Licenses}} -
    {{svg "octicon-law" 16 "mr-3"}} {{.}}
    +
    {{svg "octicon-law" 16 "mr-3"}} {{.}}
    {{end}} {{end}} {{end}} diff --git a/templates/repo/packages/metadata/npm.tmpl b/templates/repo/packages/metadata/npm.tmpl index 702fc94d6ed97..381aab099e0df 100644 --- a/templates/repo/packages/metadata/npm.tmpl +++ b/templates/repo/packages/metadata/npm.tmpl @@ -1,5 +1,5 @@ {{if eq .Package.Type 2}} - {{if .Metadata.Author}}
    {{svg "octicon-person" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.author"}}: {{.Metadata.Author}}
    {{end}} - {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} - {{if .Metadata.License}}
    {{svg "octicon-law" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.license"}}: {{.Metadata.License}}
    {{end}} + {{if .Metadata.Author}}
    {{svg "octicon-person" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.author"}}: {{.Metadata.Author}}
    {{end}} + {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} + {{if .Metadata.License}}
    {{svg "octicon-law" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.license"}}: {{.Metadata.License}}
    {{end}} {{end}} diff --git a/templates/repo/packages/metadata/nuget.tmpl b/templates/repo/packages/metadata/nuget.tmpl index 2ef835826a3ad..5e9c0da13d380 100644 --- a/templates/repo/packages/metadata/nuget.tmpl +++ b/templates/repo/packages/metadata/nuget.tmpl @@ -1,4 +1,4 @@ {{if eq .Package.Type 1}} - {{if .Metadata.Authors}}
    {{svg "octicon-person" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.author"}}: {{.Metadata.Authors}}
    {{end}} - {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} + {{if .Metadata.Authors}}
    {{svg "octicon-person" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.author"}}: {{.Metadata.Authors}}
    {{end}} + {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} {{end}} diff --git a/templates/repo/packages/metadata/pypi.tmpl b/templates/repo/packages/metadata/pypi.tmpl index 113b0c92bac92..25ec8202bcc08 100644 --- a/templates/repo/packages/metadata/pypi.tmpl +++ b/templates/repo/packages/metadata/pypi.tmpl @@ -1,6 +1,6 @@ {{if eq .Package.Type 4}} - {{if .Metadata.Author}}
    {{svg "octicon-person" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.author"}}: {{.Metadata.Author}}
    {{end}} - {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} - {{if .Metadata.License}}
    {{svg "octicon-law" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.license"}}: {{.Metadata.License}}
    {{end}} - {{if .Metadata.RequiresPython}}
    {{.i18n.Tr "repo.packages.python.requires"}}: {{.Metadata.RequiresPython}}
    {{end}} + {{if .Metadata.Author}}
    {{svg "octicon-person" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.author"}}: {{.Metadata.Author}}
    {{end}} + {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} + {{if .Metadata.License}}
    {{svg "octicon-law" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.license"}}: {{.Metadata.License}}
    {{end}} + {{if .Metadata.RequiresPython}}
    {{.i18n.Tr "repo.packages.pypi.requires"}}: {{.Metadata.RequiresPython}}
    {{end}} {{end}} diff --git a/templates/repo/packages/view.tmpl b/templates/repo/packages/view.tmpl index 72b6ad2f8810f..b55ce57339eec 100644 --- a/templates/repo/packages/view.tmpl +++ b/templates/repo/packages/view.tmpl @@ -22,23 +22,36 @@
    {{.i18n.Tr "repo.packages.details"}} -
    {{svg "octicon-calendar" 16 "mr-3"}} {{.Package.CreatedUnix.FormatDate}}
    - {{template "repo/packages/metadata/generic" .}} - {{template "repo/packages/metadata/nuget" .}} - {{template "repo/packages/metadata/npm" .}} - {{template "repo/packages/metadata/maven" .}} - {{template "repo/packages/metadata/pypi" .}} +
    +
    + {{svg "octicon-calendar" 16 "mr-3"}} {{.Package.CreatedUnix.FormatDate}} +
    + {{template "repo/packages/metadata/generic" .}} + {{template "repo/packages/metadata/nuget" .}} + {{template "repo/packages/metadata/npm" .}} + {{template "repo/packages/metadata/maven" .}} + {{template "repo/packages/metadata/pypi" .}} +
    {{.i18n.Tr "repo.packages.assets"}} +
    {{range .Files}} - +
    + {{.Name}} +
    {{end}} +
    {{if .OtherVersions}}
    {{.i18n.Tr "repo.packages.versions"}} +
    {{range .OtherVersions}} -
    {{.Version}} {{$.i18n.Tr "repo.packages.versions.on"}} {{.CreatedUnix.FormatDate}}
    +
    + {{.Version}} + {{$.i18n.Tr "repo.packages.versions.on"}} {{.CreatedUnix.FormatDate}} +
    {{end}} +
    {{end}} {{if .CanWritePackages}}
    From 7264b7e060f682cebd4ba7f1069936d46b85cad2 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 10 Aug 2021 20:13:00 +0200 Subject: [PATCH 026/130] Added package webhook. --- models/package.go | 4 +- models/webhook.go | 11 ++++ models/webhook_test.go | 1 + modules/convert/package.go | 51 +++++++++++++++++++ modules/notification/base/notifier.go | 3 ++ modules/notification/base/null.go | 8 +++ modules/notification/notification.go | 14 +++++ modules/notification/webhook/webhook.go | 30 ++++++++++- modules/structs/hook.go | 25 +++++++++ modules/structs/package.go | 38 ++++++++++++++ options/locale/locale_en-US.ini | 2 + routers/api/v1/packages/generic/generic.go | 2 +- routers/api/v1/packages/nuget/nuget.go | 2 +- routers/web/repo/packages.go | 2 +- routers/web/repo/webhook.go | 1 + services/forms/repo_form.go | 1 + services/packages/packages.go | 16 ++++-- templates/repo/settings/webhook/settings.tmpl | 10 ++++ 18 files changed, 210 insertions(+), 11 deletions(-) create mode 100644 modules/convert/package.go create mode 100644 modules/structs/package.go diff --git a/models/package.go b/models/package.go index d14216b3e4def..b8c526df6eb86 100644 --- a/models/package.go +++ b/models/package.go @@ -69,7 +69,7 @@ type Package struct { UpdatedUnix timeutil.TimeStamp `xorm:"updated"` } -// LoadCreator loads poster +// LoadCreator loads the creator func (p *Package) LoadCreator() error { if p.Creator == nil { var err error @@ -272,7 +272,7 @@ func GetPackageByNameAndVersion(repositoryID int64, packageType PackageType, pac func SearchPackages(repositoryID int64, packageType PackageType, query string, skip, take int) (int64, []*Package, error) { var cond builder.Cond = builder.Eq{ "repo_id": repositoryID, - "type": packageType, + "type": packageType, } if query != "" { cond = cond.And(builder.Like{"lower_name", strings.ToLower(query)}) diff --git a/models/webhook.go b/models/webhook.go index 138ba26bde090..5dc68751dd5f7 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -97,6 +97,7 @@ type HookEvents struct { PullRequestSync bool `json:"pull_request_sync"` Repository bool `json:"repository"` Release bool `json:"release"` + Package bool `json:"package"` } // HookEvent represents events that will delivery hook. @@ -297,6 +298,12 @@ func (w *Webhook) HasRepositoryEvent() bool { (w.ChooseEvents && w.HookEvents.Repository) } +// HasPackageEvent returns if hook enabled package event. +func (w *Webhook) HasPackageEvent() bool { + return w.SendEverything || + (w.ChooseEvents && w.HookEvents.Package) +} + // EventCheckers returns event checkers func (w *Webhook) EventCheckers() []struct { Has func() bool @@ -326,6 +333,7 @@ func (w *Webhook) EventCheckers() []struct { {w.HasPullRequestSyncEvent, HookEventPullRequestSync}, {w.HasRepositoryEvent, HookEventRepository}, {w.HasReleaseEvent, HookEventRelease}, + {w.HasPackageEvent, HookEventPackage}, } } @@ -595,6 +603,7 @@ const ( HookEventPullRequestSync HookEventType = "pull_request_sync" HookEventRepository HookEventType = "repository" HookEventRelease HookEventType = "release" + HookEventPackage HookEventType = "package" ) // Event returns the HookEventType as an event string @@ -625,6 +634,8 @@ func (h HookEventType) Event() string { return "repository" case HookEventRelease: return "release" + case HookEventPackage: + return "package" } return "" } diff --git a/models/webhook_test.go b/models/webhook_test.go index 84520666c0b22..e4a26c12f2d7f 100644 --- a/models/webhook_test.go +++ b/models/webhook_test.go @@ -69,6 +69,7 @@ func TestWebhook_EventsArray(t *testing.T) { "pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone", "pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected", "pull_request_review_comment", "pull_request_sync", "repository", "release", + "package", }, (&Webhook{ HookEvent: &HookEvent{SendEverything: true}, diff --git a/modules/convert/package.go b/modules/convert/package.go new file mode 100644 index 0000000000000..3e4f8a52fe875 --- /dev/null +++ b/modules/convert/package.go @@ -0,0 +1,51 @@ +// Copyright 2021 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 convert + +import ( + "code.gitea.io/gitea/models" + api "code.gitea.io/gitea/modules/structs" +) + +// ToPackage convert a models.Package to api.Package +func ToPackage(p *models.Package) *api.Package { + if err := p.LoadCreator(); err != nil { + return &api.Package{} + } + files, err := p.GetFiles() + if err != nil { + return &api.Package{} + } + + apiFiles := make([]*api.PackageFile, 0, len(files)) + for _, file := range files { + apiFiles = append(apiFiles, ToPackageFile(file)) + } + return &api.Package{ + ID: p.ID, + Creator: ToUser(p.Creator, nil), + Type: p.Type.String(), + Name: p.Name, + Version: p.Version, + Files: apiFiles, + CreatedAt: p.CreatedUnix.AsTime(), + UpdatedAt: p.CreatedUnix.AsTime(), + } +} + +// ToPackageFile converts models.PackageFile to api.PackageFile +func ToPackageFile(pf *models.PackageFile) *api.PackageFile { + return &api.PackageFile{ + ID: pf.ID, + Size: pf.Size, + Name: pf.Name, + HashMD5: pf.HashMD5, + HashSHA1: pf.HashSHA1, + HashSHA256: pf.HashSHA256, + HashSHA512: pf.HashSHA512, + CreatedAt: pf.CreatedUnix.AsTime(), + UpdatedAt: pf.UpdatedUnix.AsTime(), + } +} diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go index 8f8aa659b45d9..6654af7443cbe 100644 --- a/modules/notification/base/notifier.go +++ b/modules/notification/base/notifier.go @@ -59,4 +59,7 @@ type Notifier interface { NotifySyncDeleteRef(doer *models.User, repo *models.Repository, refType, refFullName string) NotifyRepoPendingTransfer(doer, newOwner *models.User, repo *models.Repository) + + NotifyPackageCreate(repo *models.Repository, p *models.Package) + NotifyPackageDelete(doer *models.User, repo *models.Repository, p *models.Package) } diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go index 32fe259bca8d7..311dcc2e4a40a 100644 --- a/modules/notification/base/null.go +++ b/modules/notification/base/null.go @@ -170,3 +170,11 @@ func (*NullNotifier) NotifySyncDeleteRef(doer *models.User, repo *models.Reposit // NotifyRepoPendingTransfer places a place holder function func (*NullNotifier) NotifyRepoPendingTransfer(doer, newOwner *models.User, repo *models.Repository) { } + +// NotifyPackageCreate places a place holder function +func (*NullNotifier) NotifyPackageCreate(repo *models.Repository, p *models.Package) { +} + +// NotifyPackageDelete places a place holder function +func (*NullNotifier) NotifyPackageDelete(doer *models.User, repo *models.Repository, p *models.Package) { +} diff --git a/modules/notification/notification.go b/modules/notification/notification.go index b574f3ccda225..a06e332a12c6d 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -297,3 +297,17 @@ func NotifyRepoPendingTransfer(doer, newOwner *models.User, repo *models.Reposit notifier.NotifyRepoPendingTransfer(doer, newOwner, repo) } } + +// NotifyPackageCreate notifies creation of a package to notifiers +func NotifyPackageCreate(repo *models.Repository, p *models.Package) { + for _, notifier := range notifiers { + notifier.NotifyPackageCreate(repo, p) + } +} + +// NotifyPackageDelete notifies deletion of a package to notifiers +func NotifyPackageDelete(doer *models.User, repo *models.Repository, p *models.Package) { + for _, notifier := range notifiers { + notifier.NotifyPackageDelete(doer, repo, p) + } +} diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go index acdb91efe373e..1c784ec1b1134 100644 --- a/modules/notification/webhook/webhook.go +++ b/modules/notification/webhook/webhook.go @@ -470,7 +470,6 @@ func (m *webhookNotifier) NotifyDeleteComment(doer *models.User, comment *models if err != nil { log.Error("PrepareWebhooks [comment_id: %d]: %v", comment.ID, err) } - } func (m *webhookNotifier) NotifyIssueChangeLabels(doer *models.User, issue *models.Issue, @@ -819,3 +818,32 @@ func (m *webhookNotifier) NotifySyncCreateRef(pusher *models.User, repo *models. func (m *webhookNotifier) NotifySyncDeleteRef(pusher *models.User, repo *models.Repository, refType, refFullName string) { m.NotifyDeleteRef(pusher, repo, refType, refFullName) } + +func (m *webhookNotifier) NotifyPackageCreate(repo *models.Repository, p *models.Package) { + if err := p.LoadCreator(); err != nil { + log.Error("LoadCreator: %v", err) + } + + notifyPackage(p.Creator, repo, p, api.HookPackageCreated) +} + +func (m *webhookNotifier) NotifyPackageDelete(doer *models.User, repo *models.Repository, p *models.Package) { + notifyPackage(doer, repo, p, api.HookPackageDeleted) +} + +func notifyPackage(sender *models.User, repo *models.Repository, p *models.Package, action api.HookPackageAction) { + org := repo.MustOwner() + if !org.IsOrganization() { + org = nil + } + + if err := webhook_services.PrepareWebhooks(repo, models.HookEventPackage, &api.PackagePayload{ + Action: action, + Repository: convert.ToRepo(repo, models.AccessModeNone), + Package: convert.ToPackage(p), + Organization: convert.ToUser(org, nil), + Sender: convert.ToUser(sender, nil), + }); err != nil { + log.Error("PrepareWebhooks: %v", err) + } +} diff --git a/modules/structs/hook.go b/modules/structs/hook.go index 163fb5c94d619..89df03ce81130 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -112,6 +112,7 @@ var ( _ Payloader = &PullRequestPayload{} _ Payloader = &RepositoryPayload{} _ Payloader = &ReleasePayload{} + _ Payloader = &PackagePayload{} ) // _________ __ @@ -427,3 +428,27 @@ type RepositoryPayload struct { func (p *RepositoryPayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } + +// HookPackageAction an action that happens to a package +type HookPackageAction string + +const ( + // HookPackageCreated created + HookPackageCreated HookPackageAction = "created" + // HookPackageDeleted deleted + HookPackageDeleted HookPackageAction = "deleted" +) + +// PackagePayload represents a package payload +type PackagePayload struct { + Action HookPackageAction `json:"action"` + Repository *Repository `json:"repository"` + Package *Package `json:"package"` + Organization *User `json:"organization"` + Sender *User `json:"sender"` +} + +// JSONPayload implements Payload +func (p *PackagePayload) JSONPayload() ([]byte, error) { + return json.MarshalIndent(p, "", " ") +} diff --git a/modules/structs/package.go b/modules/structs/package.go new file mode 100644 index 0000000000000..3dab2c710ef6e --- /dev/null +++ b/modules/structs/package.go @@ -0,0 +1,38 @@ +// Copyright 2021 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 structs + +import ( + "time" +) + +// Package represents a package +type Package struct { + ID int64 `json:"id"` + Creator *User `json:"creator"` + Type string `json:"type"` + Name string `json:"name"` + Version string `json:"version"` + Files []*PackageFile `json:"files"` + // swagger:strfmt date-time + CreatedAt time.Time `json:"created_at"` + // swagger:strfmt date-time + UpdatedAt time.Time `json:"updated_at"` +} + +// PackageFile represents a package file +type PackageFile struct { + ID int64 `json:"id"` + Size int64 + Name string `json:"name"` + HashMD5 string `json:"md5"` + HashSHA1 string `json:"sha1"` + HashSHA256 string `json:"sha256"` + HashSHA512 string `json:"sha512"` + // swagger:strfmt date-time + CreatedAt time.Time `json:"created_at"` + // swagger:strfmt date-time + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index e994e34f82b9b..2a6bcc3e50369 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1864,6 +1864,8 @@ settings.event_pull_request_review = Pull Request Reviewed settings.event_pull_request_review_desc = Pull request approved, rejected, or review comment. settings.event_pull_request_sync = Pull Request Synchronized settings.event_pull_request_sync_desc = Pull request synchronized. +settings.event_package = Package +settings.event_package_desc = Package created or deleted in a repository. settings.branch_filter = Branch filter settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or *, events for all branches are reported. See github.com/gobwas/glob documentation for syntax. Examples: master, {master,release*}. settings.active = Active diff --git a/routers/api/v1/packages/generic/generic.go b/routers/api/v1/packages/generic/generic.go index 70763916a2fcc..5d62acf2c6610 100644 --- a/routers/api/v1/packages/generic/generic.go +++ b/routers/api/v1/packages/generic/generic.go @@ -110,7 +110,7 @@ func DeletePackage(ctx *context.APIContext) { return } - err = package_service.DeletePackageByNameAndVersion(ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion) + err = package_service.DeletePackageByNameAndVersion(ctx.User, ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion) if err != nil { if err == models.ErrPackageNotExist { ctx.Error(http.StatusNotFound, "", err) diff --git a/routers/api/v1/packages/nuget/nuget.go b/routers/api/v1/packages/nuget/nuget.go index 29902a67fb1b2..9bccbd022b52e 100644 --- a/routers/api/v1/packages/nuget/nuget.go +++ b/routers/api/v1/packages/nuget/nuget.go @@ -224,7 +224,7 @@ func DeletePackage(ctx *context.APIContext) { packageName := ctx.Params("id") packageVersion := ctx.Params("version") - err := package_service.DeletePackageByNameAndVersion(ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion) + err := package_service.DeletePackageByNameAndVersion(ctx.User, ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion) if err != nil { if err == models.ErrPackageNotExist { ctx.Error(http.StatusNotFound, "", err) diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index 4604c256031f1..4b4ba7f022edb 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -146,7 +146,7 @@ func ViewPackage(ctx *context.Context) { // DeletePackagePost deletes a package func DeletePackagePost(ctx *context.Context) { - err := package_service.DeletePackageByID(ctx.Repo.Repository, ctx.ParamsInt64(":id")) + err := package_service.DeletePackageByID(ctx.User, ctx.Repo.Repository, ctx.ParamsInt64(":id")) if err != nil { ctx.Flash.Error(err.Error()) } else { diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go index 79f47470f25a5..3c29725cde83e 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/webhook.go @@ -177,6 +177,7 @@ func ParseHookEvent(form forms.WebhookForm) *models.HookEvent { PullRequestReview: form.PullRequestReview, PullRequestSync: form.PullRequestSync, Repository: form.Repository, + Package: form.Package, }, BranchFilter: form.BranchFilter, } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index ee454b3622e07..0f61f4230160b 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -236,6 +236,7 @@ type WebhookForm struct { PullRequestReview bool PullRequestSync bool Repository bool + Package bool Active bool BranchFilter string `binding:"GlobPattern"` } diff --git a/services/packages/packages.go b/services/packages/packages.go index 8cfdbc1ad663e..032e8bf70afbf 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/notification" packages_module "code.gitea.io/gitea/modules/packages" ) @@ -48,6 +49,9 @@ func CreatePackage(creator *models.User, repository *models.Repository, packageT log.Error("Error inserting package: %v", err) return nil, err } + + notification.NotifyPackageCreate(repository, p) + return p, nil } @@ -108,7 +112,7 @@ func AddFileToPackage(p *models.Package, filename string, size int64, r io.Reade } // DeletePackageByNameAndVersion deletes a package and all associated files -func DeletePackageByNameAndVersion(repository *models.Repository, packageType models.PackageType, name, version string) error { +func DeletePackageByNameAndVersion(doer *models.User, repository *models.Repository, packageType models.PackageType, name, version string) error { log.Trace("Deleting package: %v, %v, %s, %s", repository.ID, packageType, name, version) p, err := models.GetPackageByNameAndVersion(repository.ID, packageType, name, version) @@ -120,11 +124,11 @@ func DeletePackageByNameAndVersion(repository *models.Repository, packageType mo return err } - return deletePackage(p) + return deletePackage(doer, repository, p) } // DeletePackageByID deletes a package and all associated files -func DeletePackageByID(repository *models.Repository, packageID int64) error { +func DeletePackageByID(doer *models.User, repository *models.Repository, packageID int64) error { log.Trace("Deleting package: %v, %v", repository.ID, packageID) p, err := models.GetPackageByID(packageID) @@ -140,10 +144,10 @@ func DeletePackageByID(repository *models.Repository, packageID int64) error { return models.ErrPackageNotExist } - return deletePackage(p) + return deletePackage(doer, repository, p) } -func deletePackage(p *models.Package) error { +func deletePackage(doer *models.User, repository *models.Repository, p *models.Package) error { pfs, err := p.GetFiles() if err != nil { log.Error("Error getting package files: %v", err) @@ -163,6 +167,8 @@ func deletePackage(p *models.Package) error { return err } + notification.NotifyPackageDelete(doer, repository, p) + return nil } diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl index 934794b539b07..8220d3f8e4b19 100644 --- a/templates/repo/settings/webhook/settings.tmpl +++ b/templates/repo/settings/webhook/settings.tmpl @@ -87,6 +87,16 @@
    + +
    +
    +
    + + + {{.i18n.Tr "repo.settings.event_package_desc"}} +
    +
    +
    From 762590029e49239fb055fd2fcadf54bbe30bacef Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 11 Aug 2021 15:33:05 +0000 Subject: [PATCH 027/130] Add package api. --- models/package.go | 60 ++--- modules/convert/package.go | 9 - modules/structs/package.go | 11 +- routers/api/v1/api.go | 6 + routers/api/v1/packages/nuget/nuget.go | 18 +- routers/api/v1/repo/package.go | 227 +++++++++++++++++++ routers/api/v1/swagger/repo.go | 21 ++ routers/web/repo/packages.go | 5 +- templates/swagger/v1_json.tmpl | 300 +++++++++++++++++++++++++ 9 files changed, 601 insertions(+), 56 deletions(-) create mode 100644 routers/api/v1/repo/package.go diff --git a/models/package.go b/models/package.go index b8c526df6eb86..8b7f3ca7b92ce 100644 --- a/models/package.go +++ b/models/package.go @@ -8,7 +8,6 @@ import ( "errors" "strings" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "xorm.io/builder" @@ -181,15 +180,13 @@ func DeletePackagesByRepositoryID(repositoryID int64) error { // PackageSearchOptions are options for GetLatestPackagesGrouped type PackageSearchOptions struct { RepoID int64 - Page int - Query string Type string + Query string + ListOptions } -// GetLatestPackagesGrouped returns a list of all packages in their latest version of the repository -func GetLatestPackagesGrouped(opts PackageSearchOptions) ([]*Package, int64, error) { +func (opts PackageSearchOptions) toConds() builder.Cond { var cond builder.Cond = builder.Eq{"package.repo_id": opts.RepoID} - cond = cond.And(builder.Expr("p2.id IS NULL")) switch opts.Type { case "generic": @@ -208,15 +205,32 @@ func GetLatestPackagesGrouped(opts PackageSearchOptions) ([]*Package, int64, err cond = cond.And(builder.Like{"package.lower_name", strings.ToLower(opts.Query)}) } + return cond +} + +// GetPackages returns a list of all packages of the repository +func GetPackages(opts PackageSearchOptions) ([]*Package, int64, error) { + sess := x.Where(opts.toConds()) + + sess = opts.setSessionPagination(sess) + + packages := make([]*Package, 0, opts.PageSize) + count, err := sess.FindAndCount(&packages) + return packages, count, err +} + +// GetLatestPackagesGrouped returns a list of all packages in their latest version of the repository +func GetLatestPackagesGrouped(opts PackageSearchOptions) ([]*Package, int64, error) { + cond := opts.toConds(). + And(builder.Expr("p2.id IS NULL")) + sess := x.Where(cond). Table("package"). Join("left", "package p2", "package.repo_id = p2.repo_id AND package.type = p2.type AND package.lower_name = p2.lower_name AND package.version < p2.version") - if opts.Page > 0 { - sess = sess.Limit(setting.UI.PackagesPagingNum, (opts.Page-1)*setting.UI.PackagesPagingNum) - } + sess = opts.setSessionPagination(sess) - packages := make([]*Package, 0, setting.UI.PackagesPagingNum) + packages := make([]*Package, 0, opts.PageSize) count, err := sess.FindAndCount(&packages) return packages, count, err } @@ -268,32 +282,6 @@ func GetPackageByNameAndVersion(repositoryID int64, packageType PackageType, pac return p, nil } -// SearchPackages searches for packages by name and can be used to navigate through the package list -func SearchPackages(repositoryID int64, packageType PackageType, query string, skip, take int) (int64, []*Package, error) { - var cond builder.Cond = builder.Eq{ - "repo_id": repositoryID, - "type": packageType, - } - if query != "" { - cond = cond.And(builder.Like{"lower_name", strings.ToLower(query)}) - } - - if take <= 0 || take > 100 { - take = 100 - } - - sess := x.Where(cond) - if skip > 0 { - sess = sess.Limit(take, skip) - } else { - sess = sess.Limit(take) - } - - packages := make([]*Package, 0, take) - count, err := sess.FindAndCount(&packages) - return count, packages, err -} - // TryInsertPackageFile inserts a package file func TryInsertPackageFile(pf *PackageFile) (*PackageFile, error) { sess := x.NewSession() diff --git a/modules/convert/package.go b/modules/convert/package.go index 3e4f8a52fe875..6d0b82ce23d13 100644 --- a/modules/convert/package.go +++ b/modules/convert/package.go @@ -14,22 +14,13 @@ func ToPackage(p *models.Package) *api.Package { if err := p.LoadCreator(); err != nil { return &api.Package{} } - files, err := p.GetFiles() - if err != nil { - return &api.Package{} - } - apiFiles := make([]*api.PackageFile, 0, len(files)) - for _, file := range files { - apiFiles = append(apiFiles, ToPackageFile(file)) - } return &api.Package{ ID: p.ID, Creator: ToUser(p.Creator, nil), Type: p.Type.String(), Name: p.Name, Version: p.Version, - Files: apiFiles, CreatedAt: p.CreatedUnix.AsTime(), UpdatedAt: p.CreatedUnix.AsTime(), } diff --git a/modules/structs/package.go b/modules/structs/package.go index 3dab2c710ef6e..0ed7232c3f750 100644 --- a/modules/structs/package.go +++ b/modules/structs/package.go @@ -10,12 +10,11 @@ import ( // Package represents a package type Package struct { - ID int64 `json:"id"` - Creator *User `json:"creator"` - Type string `json:"type"` - Name string `json:"name"` - Version string `json:"version"` - Files []*PackageFile `json:"files"` + ID int64 `json:"id"` + Creator *User `json:"creator"` + Type string `json:"type"` + Name string `json:"name"` + Version string `json:"version"` // swagger:strfmt date-time CreatedAt time.Time `json:"created_at"` // swagger:strfmt date-time diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 7c8380dc46741..13db69e377ab8 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1020,6 +1020,12 @@ func Routes() *web.Route { m.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile) m.Get("/simple/{id}", pypi.PackageMetadata) }, reqBasicAuth()) + m.Group("/{id}", func() { + m.Get("/", repo.GetPackage) + m.Delete("/", repo.DeletePackage) + m.Get("/files", repo.ListPackageFiles) + }, reqAnyRepoReader()) + m.Get("/", reqAnyRepoReader(), repo.ListPackages) }) }, repoAssignment()) }) diff --git a/routers/api/v1/packages/nuget/nuget.go b/routers/api/v1/packages/nuget/nuget.go index 9bccbd022b52e..5f7788c9470d0 100644 --- a/routers/api/v1/packages/nuget/nuget.go +++ b/routers/api/v1/packages/nuget/nuget.go @@ -31,11 +31,21 @@ func ServiceIndex(ctx *context.APIContext) { // SearchService https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages func SearchService(ctx *context.APIContext) { - query := ctx.FormTrim("q") skip := ctx.FormInt("skip") take := ctx.FormInt("take") - - total, packages, err := models.SearchPackages(ctx.Repo.Repository.ID, models.PackageNuGet, query, skip, take) + if take <= 0 { + take = setting.API.DefaultPagingNum + } + + packages, count, err := models.GetPackages(models.PackageSearchOptions{ + RepoID: ctx.Repo.Repository.ID, + Type: "nuget", + Query: ctx.FormTrim("q"), + ListOptions: models.ListOptions{ + Page: skip / take, + PageSize: take, + }, + }) if err != nil { ctx.Error(http.StatusInternalServerError, "", err) return @@ -49,7 +59,7 @@ func SearchService(ctx *context.APIContext) { resp := createSearchResultResponse( &linkBuilder{setting.AppURL + "api/v1/repos/" + ctx.Repo.Repository.FullName() + "/packages/nuget"}, - total, + count, nugetPackages, ) diff --git a/routers/api/v1/repo/package.go b/routers/api/v1/repo/package.go new file mode 100644 index 0000000000000..167872513c2fa --- /dev/null +++ b/routers/api/v1/repo/package.go @@ -0,0 +1,227 @@ +// Copyright 2021 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 repo + +import ( + "fmt" + "net/http" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/routers/api/v1/utils" + package_service "code.gitea.io/gitea/services/packages" +) + +// ListPackages gets all packages of a repository +func ListPackages(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/packages repository repoListPackages + // --- + // summary: Gets all packages of a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // - name: package_type + // in: query + // description: page size of results + // schema: + // type: string + // enum: [generic, nuget, npm, maven, pypi] + // - name: q + // in: query + // description: name filter + // type: string + // responses: + // "200": + // "$ref": "#/responses/PackageList" + + listOptions := utils.GetListOptions(ctx) + + packageType := ctx.FormTrim("package_type") + query := ctx.FormTrim("q") + + repo := ctx.Repo.Repository + + packages, count, err := models.GetPackages(models.PackageSearchOptions{ + RepoID: repo.ID, + Type: packageType, + Query: query, + ListOptions: listOptions, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetPackages", err) + return + } + + apiPackages := make([]*api.Package, 0, len(packages)) + for _, p := range packages { + if err := p.LoadCreator(); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadCreator", err) + return + } + apiPackages = append(apiPackages, convert.ToPackage(p)) + } + + ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.Header().Set("X-Total-Count", fmt.Sprint(count)) + ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") + ctx.JSON(http.StatusOK, apiPackages) +} + +// GetPackage gets a package +func GetPackage(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/packages/{id} repository repoGetPackage + // --- + // summary: Gets a package + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: id + // in: path + // description: id of the package + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/Package" + // "404": + // "$ref": "#/responses/notFound" + + p, err := models.GetPackageByID(ctx.ParamsInt64(":id")) + if err != nil { + if err == models.ErrPackageNotExist { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetPackageByID", err) + } + return + } + + ctx.JSON(http.StatusOK, convert.ToPackage(p)) +} + +// DeletePackage delete a package from a repository +func DeletePackage(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/packages/{id} repository repoDeletePackage + // --- + // summary: Delete a package from a repository + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: id + // in: path + // description: id of the package to delete + // type: integer + // format: int64 + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + err := package_service.DeletePackageByID(ctx.User, ctx.Repo.Repository, ctx.ParamsInt64(":id")) + if err != nil { + if err == models.ErrPackageNotExist { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "DeletePackageByID", err) + } + return + } + ctx.Status(http.StatusNoContent) +} + +// ListPackageFiles gets all files of a package +func ListPackageFiles(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/packages/{id}/files repository repoListPackageFiles + // --- + // summary: Gets all files of a package + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: id + // in: path + // description: id of the package to delete + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/PackageFileList" + // "404": + // "$ref": "#/responses/notFound" + + p, err := models.GetPackageByID(ctx.ParamsInt64(":id")) + if err != nil { + if err == models.ErrPackageNotExist { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetPackageByID", err) + } + return + } + + files, err := p.GetFiles() + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetFiles", err) + return + } + + apiPackageFiles := make([]*api.PackageFile, 0, len(files)) + for _, pf := range files { + apiPackageFiles = append(apiPackageFiles, convert.ToPackageFile(pf)) + } + + ctx.JSON(http.StatusOK, apiPackageFiles) +} diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index d539bcb9feabb..e9d9b7f946d27 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -316,3 +316,24 @@ type swaggerCombinedStatus struct { // in: body Body api.CombinedStatus `json:"body"` } + +// Package +// swagger:response Package +type swaggerResponsePackage struct { + // in:body + Body api.Package `json:"body"` +} + +// PackageList +// swagger:response PackageList +type swaggerResponsePackageList struct { + // in:body + Body []api.Package `json:"body"` +} + +// PackageFileList +// swagger:response PackageFileList +type swaggerResponsePackageFileList struct { + // in:body + Body []api.PackageFile `json:"body"` +} diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index 4b4ba7f022edb..b3f6ae000b436 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -50,8 +50,11 @@ func Packages(ctx *context.Context) { packages, count, err := models.GetLatestPackagesGrouped(models.PackageSearchOptions{ RepoID: repo.ID, Query: query, - Page: page, Type: packageType, + ListOptions: models.ListOptions{ + Page: page, + PageSize: setting.UI.PackagesPagingNum, + }, }) if err != nil { ctx.ServerError("GetLatestPackagesGrouped", err) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index a1d92abec7a86..7d9b6ba38dc6f 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -7005,6 +7005,199 @@ } } }, + "/repos/{owner}/{repo}/packages": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Gets all packages of a repository", + "operationId": "repoListPackages", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + }, + { + "description": "page size of results", + "name": "package_type", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "generic", + "nuget", + "npm", + "maven", + "pypi" + ] + } + }, + { + "type": "string", + "description": "name filter", + "name": "q", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/PackageList" + } + } + } + }, + "/repos/{owner}/{repo}/packages/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Gets a package", + "operationId": "repoGetPackage", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "id of the package", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/Package" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "tags": [ + "repository" + ], + "summary": "Delete a package from a repository", + "operationId": "repoDeletePackage", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "id of the package to delete", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/repos/{owner}/{repo}/packages/{id}/files": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Gets all package files of a package", + "operationId": "repoListPackageFiles", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "id of the package to delete", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/PackageFileList" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/pulls": { "get": { "produces": [ @@ -15637,6 +15830,89 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "Package": { + "description": "Package represents a package", + "type": "object", + "properties": { + "created_at": { + "type": "string", + "format": "date-time", + "x-go-name": "CreatedAt" + }, + "creator": { + "$ref": "#/definitions/User" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "type": { + "type": "string", + "x-go-name": "Type" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "x-go-name": "UpdatedAt" + }, + "version": { + "type": "string", + "x-go-name": "Version" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "PackageFile": { + "description": "PackageFile represents a package file", + "type": "object", + "properties": { + "Size": { + "type": "integer", + "format": "int64" + }, + "created_at": { + "type": "string", + "format": "date-time", + "x-go-name": "CreatedAt" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "md5": { + "type": "string", + "x-go-name": "HashMD5" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "sha1": { + "type": "string", + "x-go-name": "HashSHA1" + }, + "sha256": { + "type": "string", + "x-go-name": "HashSHA256" + }, + "sha512": { + "type": "string", + "x-go-name": "HashSHA512" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "x-go-name": "UpdatedAt" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "PayloadCommit": { "description": "PayloadCommit represents a commit", "type": "object", @@ -17463,6 +17739,30 @@ } } }, + "Package": { + "description": "Package", + "schema": { + "$ref": "#/definitions/Package" + } + }, + "PackageFileList": { + "description": "PackageFileList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/PackageFile" + } + } + }, + "PackageList": { + "description": "PackageList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Package" + } + } + }, "PublicKey": { "description": "PublicKey", "schema": { From 53a47deef4cd7a28d41ab08f3376b85448938e11 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 11 Aug 2021 15:57:50 +0000 Subject: [PATCH 028/130] Fixed swagger file. --- templates/swagger/v1_json.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 15513c19f211c..f7000759dbe5c 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -7208,7 +7208,7 @@ "tags": [ "repository" ], - "summary": "Gets all package files of a package", + "summary": "Gets all files of a package", "operationId": "repoListPackageFiles", "parameters": [ { From e6075abf73be137487cfde2a861f9e042e4a8dfb Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 11 Aug 2021 16:15:06 +0000 Subject: [PATCH 029/130] Fixed enum and comments. --- routers/api/v1/repo/package.go | 11 +++++------ templates/swagger/v1_json.tmpl | 26 ++++++++++++-------------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/routers/api/v1/repo/package.go b/routers/api/v1/repo/package.go index 167872513c2fa..0a7cdaafd1fed 100644 --- a/routers/api/v1/repo/package.go +++ b/routers/api/v1/repo/package.go @@ -44,10 +44,9 @@ func ListPackages(ctx *context.APIContext) { // type: integer // - name: package_type // in: query - // description: page size of results - // schema: - // type: string - // enum: [generic, nuget, npm, maven, pypi] + // description: package type filter + // type: string + // enum: [generic, nuget, npm, maven, pypi] // - name: q // in: query // description: name filter @@ -150,7 +149,7 @@ func DeletePackage(ctx *context.APIContext) { // required: true // - name: id // in: path - // description: id of the package to delete + // description: id of the package // type: integer // format: int64 // required: true @@ -192,7 +191,7 @@ func ListPackageFiles(ctx *context.APIContext) { // required: true // - name: id // in: path - // description: id of the package to delete + // description: id of the package // type: integer // format: int64 // required: true diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index f7000759dbe5c..301388db523c4 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -7089,19 +7089,17 @@ "in": "query" }, { - "description": "page size of results", + "enum": [ + "generic", + "nuget", + "npm", + "maven", + "pypi" + ], + "type": "string", + "description": "package type filter", "name": "package_type", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "generic", - "nuget", - "npm", - "maven", - "pypi" - ] - } + "in": "query" }, { "type": "string", @@ -7184,7 +7182,7 @@ { "type": "integer", "format": "int64", - "description": "id of the package to delete", + "description": "id of the package", "name": "id", "in": "path", "required": true @@ -7228,7 +7226,7 @@ { "type": "integer", "format": "int64", - "description": "id of the package to delete", + "description": "id of the package", "name": "id", "in": "path", "required": true From 068b1b1cf88967a316abdc4f5dd4b38ce525d712 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 11 Aug 2021 19:13:00 +0200 Subject: [PATCH 030/130] Fixed NuGet pagination. --- models/commit_status.go | 6 ++-- models/gpg_key.go | 2 +- models/issue_comment.go | 2 +- models/issue_label.go | 4 +-- models/issue_milestone.go | 2 +- models/issue_stopwatch.go | 2 +- models/issue_watch.go | 2 +- models/list_options.go | 7 ++--- models/notification.go | 2 +- models/oauth2_application.go | 2 +- models/org.go | 6 ++-- models/org_team.go | 6 ++-- models/package.go | 22 ++++++------- models/pull_list.go | 2 +- models/release.go | 2 +- models/repo.go | 4 +-- models/repo_watch.go | 2 +- models/review.go | 2 +- models/session_paginator.go | 43 ++++++++++++++++++++++++++ models/ssh_key.go | 2 +- models/ssh_key_deploy.go | 2 +- models/ssh_key_principals.go | 2 +- models/star.go | 2 +- models/token.go | 2 +- models/topic.go | 2 +- models/user.go | 10 +++--- models/webhook.go | 4 +-- routers/api/v1/packages/nuget/nuget.go | 14 +++------ routers/api/v1/repo/package.go | 10 +++--- routers/web/repo/packages.go | 4 +-- 30 files changed, 104 insertions(+), 70 deletions(-) create mode 100644 models/session_paginator.go diff --git a/models/commit_status.go b/models/commit_status.go index c27582024280f..79a7686ee90c2 100644 --- a/models/commit_status.go +++ b/models/commit_status.go @@ -97,7 +97,7 @@ func GetCommitStatuses(repo *Repository, sha string, opts *CommitStatusOptions) } countSession := listCommitStatusesStatement(repo, sha, opts) - countSession = opts.setSessionPagination(countSession) + countSession = opts.SetSessionPagination(countSession) maxResults, err := countSession.Count(new(CommitStatus)) if err != nil { log.Error("Count PRs: %v", err) @@ -106,7 +106,7 @@ func GetCommitStatuses(repo *Repository, sha string, opts *CommitStatusOptions) statuses := make([]*CommitStatus, 0, opts.PageSize) findSession := listCommitStatusesStatement(repo, sha, opts) - findSession = opts.setSessionPagination(findSession) + findSession = opts.SetSessionPagination(findSession) sortCommitStatusesSession(findSession, opts.SortType) return statuses, maxResults, findSession.Find(&statuses) } @@ -149,7 +149,7 @@ func getLatestCommitStatus(e Engine, repoID int64, sha string, listOptions ListO Select("max( id ) as id"). GroupBy("context_hash").OrderBy("max( id ) desc") - sess = listOptions.setSessionPagination(sess) + sess = listOptions.SetSessionPagination(sess) err := sess.Find(&ids) if err != nil { diff --git a/models/gpg_key.go b/models/gpg_key.go index 74ffb82a545b5..41bc784b908e2 100644 --- a/models/gpg_key.go +++ b/models/gpg_key.go @@ -64,7 +64,7 @@ func ListGPGKeys(uid int64, listOptions ListOptions) ([]*GPGKey, error) { func listGPGKeys(e Engine, uid int64, listOptions ListOptions) ([]*GPGKey, error) { sess := e.Table(&GPGKey{}).Where("owner_id=? AND primary_key_id=''", uid) if listOptions.Page != 0 { - sess = listOptions.setSessionPagination(sess) + sess = listOptions.SetSessionPagination(sess) } keys := make([]*GPGKey, 0, 2) diff --git a/models/issue_comment.go b/models/issue_comment.go index 755041efd7ed1..8788c304ee488 100644 --- a/models/issue_comment.go +++ b/models/issue_comment.go @@ -1007,7 +1007,7 @@ func findComments(e Engine, opts FindCommentsOptions) ([]*Comment, error) { } if opts.Page != 0 { - sess = opts.setSessionPagination(sess) + sess = opts.SetSessionPagination(sess) } // WARNING: If you change this order you will need to fix createCodeComment diff --git a/models/issue_label.go b/models/issue_label.go index d1ff4692366d0..60cbec90077f9 100644 --- a/models/issue_label.go +++ b/models/issue_label.go @@ -433,7 +433,7 @@ func getLabelsByRepoID(e Engine, repoID int64, sortType string, listOptions List } if listOptions.Page != 0 { - sess = listOptions.setSessionPagination(sess) + sess = listOptions.SetSessionPagination(sess) } return labels, sess.Find(&labels) @@ -545,7 +545,7 @@ func getLabelsByOrgID(e Engine, orgID int64, sortType string, listOptions ListOp } if listOptions.Page != 0 { - sess = listOptions.setSessionPagination(sess) + sess = listOptions.SetSessionPagination(sess) } return labels, sess.Find(&labels) diff --git a/models/issue_milestone.go b/models/issue_milestone.go index 5e934cde0a02f..cd6ca41531db7 100644 --- a/models/issue_milestone.go +++ b/models/issue_milestone.go @@ -399,7 +399,7 @@ func GetMilestones(opts GetMilestonesOption) (MilestoneList, error) { } if opts.Page != 0 { - sess = opts.setSessionPagination(sess) + sess = opts.SetSessionPagination(sess) } switch opts.SortType { diff --git a/models/issue_stopwatch.go b/models/issue_stopwatch.go index 8cdad94fd4ae1..e1323ffc6cc07 100644 --- a/models/issue_stopwatch.go +++ b/models/issue_stopwatch.go @@ -45,7 +45,7 @@ func GetUserStopwatches(userID int64, listOptions ListOptions) ([]*Stopwatch, er sws := make([]*Stopwatch, 0, 8) sess := x.Where("stopwatch.user_id = ?", userID) if listOptions.Page != 0 { - sess = listOptions.setSessionPagination(sess) + sess = listOptions.SetSessionPagination(sess) } err := sess.Find(&sws) diff --git a/models/issue_watch.go b/models/issue_watch.go index a3cbbf2c1d965..ac4ec638deb9e 100644 --- a/models/issue_watch.go +++ b/models/issue_watch.go @@ -111,7 +111,7 @@ func getIssueWatchers(e Engine, issueID int64, listOptions ListOptions) (IssueWa Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id") if listOptions.Page != 0 { - sess = listOptions.setSessionPagination(sess) + sess = listOptions.SetSessionPagination(sess) watches := make([]*IssueWatch, 0, listOptions.PageSize) return watches, sess.Find(&watches) } diff --git a/models/list_options.go b/models/list_options.go index ff02933f9bac4..1d465fbe77c16 100644 --- a/models/list_options.go +++ b/models/list_options.go @@ -16,18 +16,15 @@ type ListOptions struct { Page int // start from 1 } -func (opts *ListOptions) getPaginatedSession() *xorm.Session { +func (opts *ListOptions) GetPaginatedSession() *xorm.Session { opts.setDefaultValues() return x.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) } -func (opts *ListOptions) setSessionPagination(sess *xorm.Session) *xorm.Session { +func (opts *ListOptions) SetSessionPagination(sess *xorm.Session) *xorm.Session { opts.setDefaultValues() - if opts.PageSize <= 0 { - return sess - } return sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) } diff --git a/models/notification.go b/models/notification.go index c4c7728ad9f6b..e6fdaecd4e66a 100644 --- a/models/notification.go +++ b/models/notification.go @@ -110,7 +110,7 @@ func (opts *FindNotificationOptions) ToCond() builder.Cond { func (opts *FindNotificationOptions) ToSession(e Engine) *xorm.Session { sess := e.Where(opts.ToCond()) if opts.Page != 0 { - sess = opts.setSessionPagination(sess) + sess = opts.SetSessionPagination(sess) } return sess } diff --git a/models/oauth2_application.go b/models/oauth2_application.go index 2aa9fbd3d9217..81bc9636bf2b6 100644 --- a/models/oauth2_application.go +++ b/models/oauth2_application.go @@ -275,7 +275,7 @@ func ListOAuth2Applications(uid int64, listOptions ListOptions) ([]*OAuth2Applic Desc("id") if listOptions.Page != 0 { - sess = listOptions.setSessionPagination(sess) + sess = listOptions.SetSessionPagination(sess) apps := make([]*OAuth2Application, 0, listOptions.PageSize) return apps, sess.Find(&apps) diff --git a/models/org.go b/models/org.go index 58fb26b1bb511..cbe4173bbb802 100644 --- a/models/org.go +++ b/models/org.go @@ -65,7 +65,7 @@ func (org *User) getTeams(e Engine) error { // GetTeams returns paginated teams that belong to organization. func (org *User) GetTeams(opts *SearchTeamOptions) error { if opts.Page != 0 { - return org.getTeams(opts.getPaginatedSession()) + return org.getTeams(opts.GetPaginatedSession()) } return org.getTeams(x) @@ -529,7 +529,7 @@ func GetOrgUsersByUserID(uid int64, opts *SearchOrganizationsOptions) ([]*OrgUse } if opts.PageSize != 0 { - sess = opts.setSessionPagination(sess) + sess = opts.SetSessionPagination(sess) } err := sess. @@ -549,7 +549,7 @@ func getOrgUsersByOrgID(e Engine, opts *FindOrgMembersOpts) ([]*OrgUser, error) sess.And("is_public = ?", true) } if opts.ListOptions.PageSize > 0 { - sess = opts.setSessionPagination(sess) + sess = opts.SetSessionPagination(sess) ous := make([]*OrgUser, 0, opts.PageSize) return ous, sess.Find(&ous) diff --git a/models/org_team.go b/models/org_team.go index 6226772b9a828..e357c99d95bdc 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -168,7 +168,7 @@ func (t *Team) GetRepositories(opts *SearchTeamOptions) error { return t.getRepositories(x) } - return t.getRepositories(opts.getPaginatedSession()) + return t.getRepositories(opts.GetPaginatedSession()) } func (t *Team) getMembers(e Engine) (err error) { @@ -182,7 +182,7 @@ func (t *Team) GetMembers(opts *SearchMembersOptions) (err error) { return t.getMembers(x) } - return t.getMembers(opts.getPaginatedSession()) + return t.getMembers(opts.GetPaginatedSession()) } // AddMember adds new membership of the team to the organization, @@ -795,7 +795,7 @@ func getUserTeams(e Engine, userID int64, listOptions ListOptions) (teams []*Tea Join("INNER", "team_user", "team_user.team_id = team.id"). Where("team_user.uid=?", userID) if listOptions.Page != 0 { - sess = listOptions.setSessionPagination(sess) + sess = listOptions.SetSessionPagination(sess) } return teams, sess.Find(&teams) } diff --git a/models/package.go b/models/package.go index 8b7f3ca7b92ce..45851373d3ebd 100644 --- a/models/package.go +++ b/models/package.go @@ -179,13 +179,13 @@ func DeletePackagesByRepositoryID(repositoryID int64) error { // PackageSearchOptions are options for GetLatestPackagesGrouped type PackageSearchOptions struct { - RepoID int64 - Type string - Query string - ListOptions + RepoID int64 + Type string + Query string + Paginator SessionPaginator } -func (opts PackageSearchOptions) toConds() builder.Cond { +func (opts *PackageSearchOptions) toConds() builder.Cond { var cond builder.Cond = builder.Eq{"package.repo_id": opts.RepoID} switch opts.Type { @@ -209,18 +209,18 @@ func (opts PackageSearchOptions) toConds() builder.Cond { } // GetPackages returns a list of all packages of the repository -func GetPackages(opts PackageSearchOptions) ([]*Package, int64, error) { +func GetPackages(opts *PackageSearchOptions) ([]*Package, int64, error) { sess := x.Where(opts.toConds()) - sess = opts.setSessionPagination(sess) + sess = opts.Paginator.SetSessionPagination(sess) - packages := make([]*Package, 0, opts.PageSize) + packages := make([]*Package, 0, 10) count, err := sess.FindAndCount(&packages) return packages, count, err } // GetLatestPackagesGrouped returns a list of all packages in their latest version of the repository -func GetLatestPackagesGrouped(opts PackageSearchOptions) ([]*Package, int64, error) { +func GetLatestPackagesGrouped(opts *PackageSearchOptions) ([]*Package, int64, error) { cond := opts.toConds(). And(builder.Expr("p2.id IS NULL")) @@ -228,9 +228,9 @@ func GetLatestPackagesGrouped(opts PackageSearchOptions) ([]*Package, int64, err Table("package"). Join("left", "package p2", "package.repo_id = p2.repo_id AND package.type = p2.type AND package.lower_name = p2.lower_name AND package.version < p2.version") - sess = opts.setSessionPagination(sess) + sess = opts.Paginator.SetSessionPagination(sess) - packages := make([]*Package, 0, opts.PageSize) + packages := make([]*Package, 0, 10) count, err := sess.FindAndCount(&packages) return packages, count, err } diff --git a/models/pull_list.go b/models/pull_list.go index 2f685e19f5eaa..e9caa0fd4108e 100644 --- a/models/pull_list.go +++ b/models/pull_list.go @@ -100,7 +100,7 @@ func PullRequests(baseRepoID int64, opts *PullRequestsOptions) ([]*PullRequest, log.Error("listPullRequestStatement: %v", err) return nil, maxResults, err } - findSession = opts.setSessionPagination(findSession) + findSession = opts.SetSessionPagination(findSession) prs := make([]*PullRequest, 0, opts.PageSize) return prs, maxResults, findSession.Find(&prs) } diff --git a/models/release.go b/models/release.go index 1ce88a8210c9d..cefd1a3eb2732 100644 --- a/models/release.go +++ b/models/release.go @@ -208,7 +208,7 @@ func GetReleasesByRepoID(repoID int64, opts FindReleasesOptions) ([]*Release, er Where(opts.toConds(repoID)) if opts.PageSize != 0 { - sess = opts.setSessionPagination(sess) + sess = opts.SetSessionPagination(sess) } rels := make([]*Release, 0, opts.PageSize) diff --git a/models/repo.go b/models/repo.go index 93827f6a8842c..0c42528d26eed 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1736,7 +1736,7 @@ func GetUserRepositories(opts *SearchRepoOptions) ([]*Repository, int64, error) sess.Where(cond).OrderBy(opts.OrderBy.String()) repos := make([]*Repository, 0, opts.PageSize) - return repos, count, opts.setSessionPagination(sess).Find(&repos) + return repos, count, opts.SetSessionPagination(sess).Find(&repos) } // GetUserMirrorRepositories returns a list of mirror repositories of given user. @@ -2027,7 +2027,7 @@ func (repo *Repository) GetForks(listOptions ListOptions) ([]*Repository, error) return forks, x.Find(&forks, &Repository{ForkID: repo.ID}) } - sess := listOptions.getPaginatedSession() + sess := listOptions.GetPaginatedSession() forks := make([]*Repository, 0, listOptions.PageSize) return forks, sess.Find(&forks, &Repository{ForkID: repo.ID}) } diff --git a/models/repo_watch.go b/models/repo_watch.go index 656696b34f2e8..75ae43f312fc2 100644 --- a/models/repo_watch.go +++ b/models/repo_watch.go @@ -165,7 +165,7 @@ func (repo *Repository) GetWatchers(opts ListOptions) ([]*User, error) { Join("LEFT", "watch", "`user`.id=`watch`.user_id"). And("`watch`.mode<>?", RepoWatchModeDont) if opts.Page > 0 { - sess = opts.setSessionPagination(sess) + sess = opts.SetSessionPagination(sess) users := make([]*User, 0, opts.PageSize) return users, sess.Find(&users) diff --git a/models/review.go b/models/review.go index acb54d970fdc8..8f5e848977e2b 100644 --- a/models/review.go +++ b/models/review.go @@ -195,7 +195,7 @@ func findReviews(e Engine, opts FindReviewOptions) ([]*Review, error) { reviews := make([]*Review, 0, 10) sess := e.Where(opts.toCond()) if opts.Page > 0 { - sess = opts.ListOptions.setSessionPagination(sess) + sess = opts.ListOptions.SetSessionPagination(sess) } return reviews, sess. Asc("created_unix"). diff --git a/models/session_paginator.go b/models/session_paginator.go new file mode 100644 index 0000000000000..b6d7cadb56276 --- /dev/null +++ b/models/session_paginator.go @@ -0,0 +1,43 @@ +// Copyright 2021 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 models + +import ( + "code.gitea.io/gitea/modules/setting" + + "xorm.io/xorm" +) + +type SessionPaginator interface { + GetPaginatedSession() *xorm.Session + SetSessionPagination(sess *xorm.Session) *xorm.Session +} + +// AbsoluteSessionPaginator to paginate results +type AbsoluteSessionPaginator struct { + Skip int + Take int +} + +func (opts *AbsoluteSessionPaginator) GetPaginatedSession() *xorm.Session { + opts.setDefaultValues() + + return x.Limit(opts.Take, opts.Skip) +} + +func (opts *AbsoluteSessionPaginator) SetSessionPagination(sess *xorm.Session) *xorm.Session { + opts.setDefaultValues() + + return sess.Limit(opts.Take, opts.Skip) +} + +func (opts *AbsoluteSessionPaginator) setDefaultValues() { + if opts.Take <= 0 { + opts.Take = setting.API.DefaultPagingNum + } + if opts.Take > setting.API.MaxResponseItems { + opts.Take = setting.API.MaxResponseItems + } +} diff --git a/models/ssh_key.go b/models/ssh_key.go index 6cda4f1658fb2..21684c718fd99 100644 --- a/models/ssh_key.go +++ b/models/ssh_key.go @@ -195,7 +195,7 @@ func SearchPublicKey(uid int64, fingerprint string) ([]*PublicKey, error) { func ListPublicKeys(uid int64, listOptions ListOptions) ([]*PublicKey, error) { sess := x.Where("owner_id = ? AND type != ?", uid, KeyTypePrincipal) if listOptions.Page != 0 { - sess = listOptions.setSessionPagination(sess) + sess = listOptions.SetSessionPagination(sess) keys := make([]*PublicKey, 0, listOptions.PageSize) return keys, sess.Find(&keys) diff --git a/models/ssh_key_deploy.go b/models/ssh_key_deploy.go index 3189bcf456a64..6f327d407726d 100644 --- a/models/ssh_key_deploy.go +++ b/models/ssh_key_deploy.go @@ -272,7 +272,7 @@ func ListDeployKeys(repoID int64, listOptions ListOptions) ([]*DeployKey, error) func listDeployKeys(e Engine, repoID int64, listOptions ListOptions) ([]*DeployKey, error) { sess := e.Where("repo_id = ?", repoID) if listOptions.Page != 0 { - sess = listOptions.setSessionPagination(sess) + sess = listOptions.SetSessionPagination(sess) keys := make([]*DeployKey, 0, listOptions.PageSize) return keys, sess.Find(&keys) diff --git a/models/ssh_key_principals.go b/models/ssh_key_principals.go index 3459e43c8b042..0b84ef07cabff 100644 --- a/models/ssh_key_principals.go +++ b/models/ssh_key_principals.go @@ -114,7 +114,7 @@ func CheckPrincipalKeyString(user *User, content string) (_ string, err error) { func ListPrincipalKeys(uid int64, listOptions ListOptions) ([]*PublicKey, error) { sess := x.Where("owner_id = ? AND type = ?", uid, KeyTypePrincipal) if listOptions.Page != 0 { - sess = listOptions.setSessionPagination(sess) + sess = listOptions.SetSessionPagination(sess) keys := make([]*PublicKey, 0, listOptions.PageSize) return keys, sess.Find(&keys) diff --git a/models/star.go b/models/star.go index 2d9496caf504d..0f0f31dc504e4 100644 --- a/models/star.go +++ b/models/star.go @@ -73,7 +73,7 @@ func (repo *Repository) GetStargazers(opts ListOptions) ([]*User, error) { sess := x.Where("star.repo_id = ?", repo.ID). Join("LEFT", "star", "`user`.id = star.uid") if opts.Page > 0 { - sess = opts.setSessionPagination(sess) + sess = opts.SetSessionPagination(sess) users := make([]*User, 0, opts.PageSize) return users, sess.Find(&users) diff --git a/models/token.go b/models/token.go index 357afe44a7c0b..33e39c7b5ccdb 100644 --- a/models/token.go +++ b/models/token.go @@ -106,7 +106,7 @@ func ListAccessTokens(opts ListAccessTokensOptions) ([]*AccessToken, error) { sess = sess.Desc("id") if opts.Page != 0 { - sess = opts.setSessionPagination(sess) + sess = opts.SetSessionPagination(sess) tokens := make([]*AccessToken, 0, opts.PageSize) return tokens, sess.Find(&tokens) diff --git a/models/topic.go b/models/topic.go index 19c572fefebf6..79afec5b9361c 100644 --- a/models/topic.go +++ b/models/topic.go @@ -190,7 +190,7 @@ func FindTopics(opts *FindTopicOptions) (topics []*Topic, err error) { sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") } if opts.PageSize != 0 && opts.Page != 0 { - sess = opts.setSessionPagination(sess) + sess = opts.SetSessionPagination(sess) } return topics, sess.Desc("topic.repo_count").Find(&topics) } diff --git a/models/user.go b/models/user.go index c68417a2c3265..1aa38ed9d3ed8 100644 --- a/models/user.go +++ b/models/user.go @@ -332,7 +332,7 @@ func (u *User) GetFollowers(listOptions ListOptions) ([]*User, error) { Join("LEFT", "follow", "`user`.id=follow.user_id") if listOptions.Page != 0 { - sess = listOptions.setSessionPagination(sess) + sess = listOptions.SetSessionPagination(sess) users := make([]*User, 0, listOptions.PageSize) return users, sess.Find(&users) @@ -354,7 +354,7 @@ func (u *User) GetFollowing(listOptions ListOptions) ([]*User, error) { Join("LEFT", "follow", "`user`.id=follow.follow_id") if listOptions.Page != 0 { - sess = listOptions.setSessionPagination(sess) + sess = listOptions.SetSessionPagination(sess) users := make([]*User, 0, listOptions.PageSize) return users, sess.Find(&users) @@ -1677,7 +1677,7 @@ func SearchUsers(opts *SearchUserOptions) (users []*User, _ int64, _ error) { sess := x.Where(cond).OrderBy(opts.OrderBy.String()) if opts.Page != 0 { - sess = opts.setSessionPagination(sess) + sess = opts.SetSessionPagination(sess) } users = make([]*User, 0, opts.PageSize) @@ -1693,7 +1693,7 @@ func GetStarredRepos(userID int64, private bool, listOptions ListOptions) ([]*Re } if listOptions.Page != 0 { - sess = listOptions.setSessionPagination(sess) + sess = listOptions.SetSessionPagination(sess) repos := make([]*Repository, 0, listOptions.PageSize) return repos, sess.Find(&repos) @@ -1713,7 +1713,7 @@ func GetWatchedRepos(userID int64, private bool, listOptions ListOptions) ([]*Re } if listOptions.Page != 0 { - sess = listOptions.setSessionPagination(sess) + sess = listOptions.SetSessionPagination(sess) repos := make([]*Repository, 0, listOptions.PageSize) return repos, sess.Find(&repos) diff --git a/models/webhook.go b/models/webhook.go index 5dc68751dd5f7..97f8f1f153e0e 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -413,7 +413,7 @@ func GetWebhooksByRepoID(repoID int64, listOptions ListOptions) ([]*Webhook, err return webhooks, x.Find(&webhooks, &Webhook{RepoID: repoID}) } - sess := listOptions.getPaginatedSession() + sess := listOptions.GetPaginatedSession() webhooks := make([]*Webhook, 0, listOptions.PageSize) return webhooks, sess.Find(&webhooks, &Webhook{RepoID: repoID}) @@ -439,7 +439,7 @@ func GetWebhooksByOrgID(orgID int64, listOptions ListOptions) ([]*Webhook, error return ws, x.Find(&ws, &Webhook{OrgID: orgID}) } - sess := listOptions.getPaginatedSession() + sess := listOptions.GetPaginatedSession() ws := make([]*Webhook, 0, listOptions.PageSize) return ws, sess.Find(&ws, &Webhook{OrgID: orgID}) } diff --git a/routers/api/v1/packages/nuget/nuget.go b/routers/api/v1/packages/nuget/nuget.go index 5f7788c9470d0..6bf1a92cbc673 100644 --- a/routers/api/v1/packages/nuget/nuget.go +++ b/routers/api/v1/packages/nuget/nuget.go @@ -31,19 +31,13 @@ func ServiceIndex(ctx *context.APIContext) { // SearchService https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages func SearchService(ctx *context.APIContext) { - skip := ctx.FormInt("skip") - take := ctx.FormInt("take") - if take <= 0 { - take = setting.API.DefaultPagingNum - } - - packages, count, err := models.GetPackages(models.PackageSearchOptions{ + packages, count, err := models.GetPackages(&models.PackageSearchOptions{ RepoID: ctx.Repo.Repository.ID, Type: "nuget", Query: ctx.FormTrim("q"), - ListOptions: models.ListOptions{ - Page: skip / take, - PageSize: take, + Paginator: &models.AbsoluteSessionPaginator{ + Skip: ctx.FormInt("skip"), + Take: ctx.FormInt("take"), }, }) if err != nil { diff --git a/routers/api/v1/repo/package.go b/routers/api/v1/repo/package.go index 0a7cdaafd1fed..9e7035ce7f8e8 100644 --- a/routers/api/v1/repo/package.go +++ b/routers/api/v1/repo/package.go @@ -62,11 +62,11 @@ func ListPackages(ctx *context.APIContext) { repo := ctx.Repo.Repository - packages, count, err := models.GetPackages(models.PackageSearchOptions{ - RepoID: repo.ID, - Type: packageType, - Query: query, - ListOptions: listOptions, + packages, count, err := models.GetPackages(&models.PackageSearchOptions{ + RepoID: repo.ID, + Type: packageType, + Query: query, + Paginator: &listOptions, }) if err != nil { ctx.Error(http.StatusInternalServerError, "GetPackages", err) diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index b3f6ae000b436..49421e76d4be9 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -47,11 +47,11 @@ func Packages(ctx *context.Context) { repo := ctx.Repo.Repository - packages, count, err := models.GetLatestPackagesGrouped(models.PackageSearchOptions{ + packages, count, err := models.GetLatestPackagesGrouped(&models.PackageSearchOptions{ RepoID: repo.ID, Query: query, Type: packageType, - ListOptions: models.ListOptions{ + Paginator: &models.ListOptions{ Page: page, PageSize: setting.UI.PackagesPagingNum, }, From c515aa6aca30107e981e6a6c886e872dab045d9c Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 11 Aug 2021 19:14:00 +0200 Subject: [PATCH 031/130] Print test names. --- integrations/api_packages_generic_test.go | 12 ++++++++++++ integrations/api_packages_maven_test.go | 14 +++++++++++++ integrations/api_packages_npm_test.go | 8 ++++++++ integrations/api_packages_nuget_test.go | 24 +++++++++++++++++++++++ integrations/api_packages_pypi_test.go | 12 ++++++++++++ 5 files changed, 70 insertions(+) diff --git a/integrations/api_packages_generic_test.go b/integrations/api_packages_generic_test.go index aec78eb3c8fbb..1d42b1e22d053 100644 --- a/integrations/api_packages_generic_test.go +++ b/integrations/api_packages_generic_test.go @@ -30,6 +30,8 @@ func TestPackageGeneric(t *testing.T) { url := fmt.Sprintf("/api/v1/repos/%s/%s/packages/generic/%s/%s/%s?token=%s", user.Name, repository.Name, packageName, packageVersion, filename, token) t.Run("Upload", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)) MakeRequest(t, req, http.StatusCreated) @@ -47,11 +49,15 @@ func TestPackageGeneric(t *testing.T) { }) t.Run("UploadExists", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)) MakeRequest(t, req, http.StatusBadRequest) }) t.Run("Download", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequest(t, "GET", url) resp := MakeRequest(t, req, http.StatusOK) @@ -59,6 +65,8 @@ func TestPackageGeneric(t *testing.T) { }) t.Run("Delete", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequest(t, "DELETE", url) MakeRequest(t, req, http.StatusOK) @@ -68,11 +76,15 @@ func TestPackageGeneric(t *testing.T) { }) t.Run("DownloadNotExists", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequest(t, "GET", url) MakeRequest(t, req, http.StatusNotFound) }) t.Run("DeleteNotExists", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequest(t, "DELETE", url) MakeRequest(t, req, http.StatusNotFound) }) diff --git a/integrations/api_packages_maven_test.go b/integrations/api_packages_maven_test.go index 0d39428f25ed3..1ae4dff50f1b0 100644 --- a/integrations/api_packages_maven_test.go +++ b/integrations/api_packages_maven_test.go @@ -38,6 +38,8 @@ func TestPackageMaven(t *testing.T) { } t.Run("Upload", func(t *testing.T) { + defer PrintCurrentTest(t)() + putFile(t, fmt.Sprintf("/%s/%s", packageVersion, filename), "test", http.StatusCreated) putFile(t, "/maven-metadata.xml", "test", http.StatusOK) @@ -55,10 +57,14 @@ func TestPackageMaven(t *testing.T) { }) t.Run("UploadExists", func(t *testing.T) { + defer PrintCurrentTest(t)() + putFile(t, fmt.Sprintf("/%s/%s", packageVersion, filename), "test", http.StatusBadRequest) }) t.Run("Download", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)) req = AddBasicAuthHeader(req, user.Name) resp := MakeRequest(t, req, http.StatusOK) @@ -67,15 +73,23 @@ func TestPackageMaven(t *testing.T) { }) t.Run("UploadVerifySHA1", func(t *testing.T) { + defer PrintCurrentTest(t)() + t.Run("Missmatch", func(t *testing.T) { + defer PrintCurrentTest(t)() + putFile(t, fmt.Sprintf("/%s/%s.sha1", packageVersion, filename), "test", http.StatusBadRequest) }) t.Run("Valid", func(t *testing.T) { + defer PrintCurrentTest(t)() + putFile(t, fmt.Sprintf("/%s/%s.sha1", packageVersion, filename), "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", http.StatusOK) }) }) t.Run("UploadPOM", func(t *testing.T) { + defer PrintCurrentTest(t)() + pomContent := ` ` + groupID + ` diff --git a/integrations/api_packages_npm_test.go b/integrations/api_packages_npm_test.go index cc3443d1a19ec..5aa955479c4d3 100644 --- a/integrations/api_packages_npm_test.go +++ b/integrations/api_packages_npm_test.go @@ -59,6 +59,8 @@ func TestPackageNPM(t *testing.T) { filename := fmt.Sprintf("%s-%s.tgz", strings.Split(packageName, "/")[1], packageVersion) t.Run("Upload", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequestWithBody(t, "PUT", root, strings.NewReader(upload)) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusCreated) @@ -77,12 +79,16 @@ func TestPackageNPM(t *testing.T) { }) t.Run("UploadExists", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequestWithBody(t, "PUT", root, strings.NewReader(upload)) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusBadRequest) }) t.Run("Download", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequest(t, "GET", fmt.Sprintf("%s/-/%s/%s", root, packageVersion, filename)) req = AddBasicAuthHeader(req, user.Name) resp := MakeRequest(t, req, http.StatusOK) @@ -92,6 +98,8 @@ func TestPackageNPM(t *testing.T) { }) t.Run("PackageMetadata", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequest(t, "GET", root) req = AddBasicAuthHeader(req, user.Name) resp := MakeRequest(t, req, http.StatusOK) diff --git a/integrations/api_packages_nuget_test.go b/integrations/api_packages_nuget_test.go index 4d2de4ba0b121..a2620a8255e41 100644 --- a/integrations/api_packages_nuget_test.go +++ b/integrations/api_packages_nuget_test.go @@ -46,6 +46,8 @@ func TestPackageNuGet(t *testing.T) { url := fmt.Sprintf("/api/v1/repos/%s/%s/packages/nuget", user.Name, repository.Name) t.Run("ServiceIndex", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url)) req = AddBasicAuthHeader(req, user.Name) resp := MakeRequest(t, req, http.StatusOK) @@ -80,6 +82,8 @@ func TestPackageNuGet(t *testing.T) { }) t.Run("Upload", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusCreated) @@ -98,12 +102,16 @@ func TestPackageNuGet(t *testing.T) { }) t.Run("UploadExists", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusBadRequest) }) t.Run("Download", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", url, packageName, packageVersion, packageName, packageVersion)) req = AddBasicAuthHeader(req, user.Name) resp := MakeRequest(t, req, http.StatusOK) @@ -112,6 +120,8 @@ func TestPackageNuGet(t *testing.T) { }) t.Run("SearchService", func(t *testing.T) { + defer PrintCurrentTest(t)() + cases := []struct { Query string Skip int @@ -140,11 +150,15 @@ func TestPackageNuGet(t *testing.T) { }) t.Run("RegistrationService", func(t *testing.T) { + defer PrintCurrentTest(t)() + indexURL := fmt.Sprintf("%s%s/registration/%s/index.json", setting.AppURL, url[1:], packageName) leafURL := fmt.Sprintf("%s%s/registration/%s/%s.json", setting.AppURL, url[1:], packageName, packageVersion) contentURL := fmt.Sprintf("%s%s/package/%s/%s/%s.%s.nupkg", setting.AppURL, url[1:], packageName, packageVersion, packageName, packageVersion) t.Run("RegistrationIndex", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequest(t, "GET", fmt.Sprintf("%s/registration/%s/index.json", url, packageName)) req = AddBasicAuthHeader(req, user.Name) resp := MakeRequest(t, req, http.StatusOK) @@ -169,6 +183,8 @@ func TestPackageNuGet(t *testing.T) { }) t.Run("RegistrationLeaf", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequest(t, "GET", fmt.Sprintf("%s/registration/%s/%s.json", url, packageName, packageVersion)) req = AddBasicAuthHeader(req, user.Name) resp := MakeRequest(t, req, http.StatusOK) @@ -183,6 +199,8 @@ func TestPackageNuGet(t *testing.T) { }) t.Run("PackageService", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/index.json", url, packageName)) req = AddBasicAuthHeader(req, user.Name) resp := MakeRequest(t, req, http.StatusOK) @@ -195,6 +213,8 @@ func TestPackageNuGet(t *testing.T) { }) t.Run("Delete", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequest(t, "DELETE", fmt.Sprintf("%s/package/%s/%s", url, packageName, packageVersion)) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusOK) @@ -205,12 +225,16 @@ func TestPackageNuGet(t *testing.T) { }) t.Run("DownloadNotExists", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", url, packageName, packageVersion, packageName, packageVersion)) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusNotFound) }) t.Run("DeleteNotExists", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequest(t, "DELETE", fmt.Sprintf("%s/package/%s/%s", url, packageName, packageVersion)) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusNotFound) diff --git a/integrations/api_packages_pypi_test.go b/integrations/api_packages_pypi_test.go index 5c3b9d4015bae..73c4d423b5f36 100644 --- a/integrations/api_packages_pypi_test.go +++ b/integrations/api_packages_pypi_test.go @@ -57,6 +57,8 @@ func TestPackagePyPI(t *testing.T) { } t.Run("Upload", func(t *testing.T) { + defer PrintCurrentTest(t)() + filename := "test.whl" uploadFile(t, filename, content, http.StatusCreated) @@ -74,6 +76,8 @@ func TestPackagePyPI(t *testing.T) { }) t.Run("UploadAddFile", func(t *testing.T) { + defer PrintCurrentTest(t)() + filename := "test.tar.gz" uploadFile(t, filename, content, http.StatusCreated) @@ -95,16 +99,22 @@ func TestPackagePyPI(t *testing.T) { }) t.Run("UploadHashMismatch", func(t *testing.T) { + defer PrintCurrentTest(t)() + filename := "test2.whl" uploadFile(t, filename, "dummy", http.StatusBadRequest) }) t.Run("UploadExists", func(t *testing.T) { + defer PrintCurrentTest(t)() + uploadFile(t, "test.whl", content, http.StatusBadRequest) uploadFile(t, "test.tar.gz", content, http.StatusBadRequest) }) t.Run("Download", func(t *testing.T) { + defer PrintCurrentTest(t)() + downloadFile := func(filename string) { req := NewRequest(t, "GET", fmt.Sprintf("%s/files/%s/%s/%s", root, packageName, packageVersion, filename)) req = AddBasicAuthHeader(req, user.Name) @@ -118,6 +128,8 @@ func TestPackagePyPI(t *testing.T) { }) t.Run("PackageMetadata", func(t *testing.T) { + defer PrintCurrentTest(t)() + req := NewRequest(t, "GET", fmt.Sprintf("%s/simple/%s", root, packageName)) req = AddBasicAuthHeader(req, user.Name) resp := MakeRequest(t, req, http.StatusOK) From 55c6b2b367ff785fc55f247415908ff3bb3f462a Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 11 Aug 2021 19:57:00 +0200 Subject: [PATCH 032/130] Added api tests. --- integrations/api_packages_generic_test.go | 2 +- integrations/api_packages_maven_test.go | 2 +- integrations/api_packages_npm_test.go | 2 +- integrations/api_packages_nuget_test.go | 2 +- integrations/api_packages_pypi_test.go | 2 +- integrations/api_packages_test.go | 104 ++++++++++++++++++++++ 6 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 integrations/api_packages_test.go diff --git a/integrations/api_packages_generic_test.go b/integrations/api_packages_generic_test.go index 1d42b1e22d053..1959ae47ecaaa 100644 --- a/integrations/api_packages_generic_test.go +++ b/integrations/api_packages_generic_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2021 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. diff --git a/integrations/api_packages_maven_test.go b/integrations/api_packages_maven_test.go index 1ae4dff50f1b0..38dbcf908ff29 100644 --- a/integrations/api_packages_maven_test.go +++ b/integrations/api_packages_maven_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2021 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. diff --git a/integrations/api_packages_npm_test.go b/integrations/api_packages_npm_test.go index 5aa955479c4d3..b7ee8b7cd744a 100644 --- a/integrations/api_packages_npm_test.go +++ b/integrations/api_packages_npm_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2021 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. diff --git a/integrations/api_packages_nuget_test.go b/integrations/api_packages_nuget_test.go index a2620a8255e41..84e1fc1b7e0f3 100644 --- a/integrations/api_packages_nuget_test.go +++ b/integrations/api_packages_nuget_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2021 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. diff --git a/integrations/api_packages_pypi_test.go b/integrations/api_packages_pypi_test.go index 73c4d423b5f36..44e9cf58344cd 100644 --- a/integrations/api_packages_pypi_test.go +++ b/integrations/api_packages_pypi_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2021 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. diff --git a/integrations/api_packages_test.go b/integrations/api_packages_test.go new file mode 100644 index 0000000000000..ab0d8f81cd509 --- /dev/null +++ b/integrations/api_packages_test.go @@ -0,0 +1,104 @@ +// Copyright 2021 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 ( + "bytes" + "fmt" + "net/http" + "testing" + + "code.gitea.io/gitea/models" + api "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" +) + +func TestPackageAPI(t *testing.T) { + defer prepareTestEnv(t)() + repository := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 15}).(*models.Repository) + user := models.AssertExistsAndLoadBean(t, &models.User{ID: repository.OwnerID}).(*models.User) + session := loginUser(t, user.Name) + token := getTokenForLoggedInUser(t, session) + + packageName := "test-package" + packageVersion := "1.0.3" + filename := "file.bin" + + url := fmt.Sprintf("/api/v1/repos/%s/%s/packages/generic/%s/%s/%s?token=%s", user.Name, repository.Name, packageName, packageVersion, filename, token) + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader([]byte{})) + MakeRequest(t, req, http.StatusCreated) + + var packageID int64 + + t.Run("ListPackages", func(t *testing.T) { + defer PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/packages?token=%s", user.Name, repository.Name, token)) + resp := MakeRequest(t, req, http.StatusOK) + + var packages []*api.Package + DecodeJSON(t, resp, &packages) + + assert.Len(t, packages, 1) + assert.Equal(t, "Generic", packages[0].Type) + assert.Equal(t, packageName, packages[0].Name) + assert.Equal(t, packageVersion, packages[0].Version) + assert.NotNil(t, packages[0].Creator) + assert.Equal(t, user.Name, packages[0].Creator.UserName) + + packageID = packages[0].ID + }) + + t.Run("GetPackage", func(t *testing.T) { + defer PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/packages/%d?token=%s", user.Name, repository.Name, 123456, token)) + MakeRequest(t, req, http.StatusNotFound) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/packages/%d?token=%s", user.Name, repository.Name, packageID, token)) + resp := MakeRequest(t, req, http.StatusOK) + + var p *api.Package + DecodeJSON(t, resp, &p) + + assert.Equal(t, "Generic", p.Type) + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageVersion, p.Version) + assert.NotNil(t, p.Creator) + assert.Equal(t, user.Name, p.Creator.UserName) + }) + + t.Run("ListPackageFiles", func(t *testing.T) { + defer PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/packages/%d/files?token=%s", user.Name, repository.Name, 123456, token)) + MakeRequest(t, req, http.StatusNotFound) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/packages/%d/files?token=%s", user.Name, repository.Name, packageID, token)) + resp := MakeRequest(t, req, http.StatusOK) + + var files []*api.PackageFile + DecodeJSON(t, resp, &files) + + assert.Len(t, files, 1) + assert.Equal(t, int64(0), files[0].Size) + assert.Equal(t, filename, files[0].Name) + assert.Equal(t, "d41d8cd98f00b204e9800998ecf8427e", files[0].HashMD5) + assert.Equal(t, "da39a3ee5e6b4b0d3255bfef95601890afd80709", files[0].HashSHA1) + assert.Equal(t, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", files[0].HashSHA256) + assert.Equal(t, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", files[0].HashSHA512) + }) + + t.Run("DeletePackage", func(t *testing.T) { + defer PrintCurrentTest(t)() + + req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/packages/%d?token=%s", user.Name, repository.Name, 123456, token)) + MakeRequest(t, req, http.StatusNotFound) + + req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/packages/%d?token=%s", user.Name, repository.Name, packageID, token)) + MakeRequest(t, req, http.StatusNoContent) + }) +} From 6fadfe92cce1d415936911ddd63cacc860bd138a Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sun, 15 Aug 2021 17:16:44 +0000 Subject: [PATCH 033/130] Fixed access level. --- routers/api/v1/api.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index c913b1ab4f508..c49773063c518 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -991,7 +991,7 @@ func Routes() *web.Route { }, reqToken()) m.Group("/maven", func() { m.Put("/*", reqRepoWriter(models.UnitTypePackages), maven.UploadPackageFile) - m.Get("/*", reqRepoWriter(models.UnitTypePackages), maven.DownloadPackageFile) + m.Get("/*", maven.DownloadPackageFile) }, reqToken()) m.Group("/nuget", func() { m.Put("/", reqRepoWriter(models.UnitTypePackages), nuget.UploadPackage) @@ -1023,11 +1023,11 @@ func Routes() *web.Route { }, reqBasicAuth()) m.Group("/{id}", func() { m.Get("/", repo.GetPackage) - m.Delete("/", repo.DeletePackage) + m.Delete("/", reqRepoWriter(models.UnitTypePackages), repo.DeletePackage) m.Get("/files", repo.ListPackageFiles) - }, reqAnyRepoReader()) - m.Get("/", reqAnyRepoReader(), repo.ListPackages) - }) + }) + m.Get("/", repo.ListPackages) + }, reqRepoReader(models.UnitTypePackages)) }, repoAssignment()) }) From e60969b5ad73baa67f8ac4969d91231e7463fdd6 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Sun, 15 Aug 2021 19:06:38 +0000 Subject: [PATCH 034/130] Fix User unmarshal. --- docs/content/doc/packages/npm.en-us.md | 4 ++-- modules/packages/npm/creator.go | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/docs/content/doc/packages/npm.en-us.md b/docs/content/doc/packages/npm.en-us.md index 3a823a47c0b7e..c676ad23cba74 100644 --- a/docs/content/doc/packages/npm.en-us.md +++ b/docs/content/doc/packages/npm.en-us.md @@ -33,7 +33,7 @@ The following examples use the `npm` tool with the scope `@test`. To register the project’s package registry you need to configure a new package source. ```shell -npm config set {scope}:registry https://gitea.example.com/api/v1/repos/{owner}/{repository}/packages/npm +npm config set {scope}:registry https://gitea.example.com/api/v1/repos/{owner}/{repository}/packages/npm/ npm config set -- '//gitea.example.com/api/v1/repos/{owner}/{repository}/packages/npm/:_authToken' "{token}" ``` @@ -47,7 +47,7 @@ npm config set -- '//gitea.example.com/api/v1/repos/{owner}/{repository}/package For example: ```shell -npm config set @test:registry https://gitea.example.com/api/v1/repos/testuser/test-repository/packages/npm +npm config set @test:registry https://gitea.example.com/api/v1/repos/testuser/test-repository/packages/npm/ npm config set -- '//gitea.example.com/api/v1/repos/testuser/test-repository/packages/npm/:_authToken' "personal_access_token" ``` diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index 5639169ac8a43..bcfaa769fa650 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -101,6 +101,31 @@ type User struct { URL string `json:"url,omitempty"` } +// UnmarshalJSON is needed because User objects can be strings or objects +func (u *User) UnmarshalJSON(data []byte) error { + switch data[0] { + case '"': + if err := json.Unmarshal(data, &u.Name); err != nil { + return err + } + case '{': + var tmp struct { + Username string `json:"username"` + Name string `json:"name"` + Email string `json:"email"` + URL string `json:"url"` + } + if err := json.Unmarshal(data, &tmp); err != nil { + return err + } + u.Username = tmp.Username + u.Name = tmp.Name + u.Email = tmp.Email + u.URL = tmp.URL + } + return nil +} + // Repository https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version type Repository struct { Type string `json:"type"` From 4ced95341c3570be9e40c33764662c05164056e3 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 23 Aug 2021 19:56:28 +0000 Subject: [PATCH 035/130] Added RubyGems package registry. --- docs/content/doc/packages/rubygems.en-us.md | 122 +++++++ integrations/api_packages_rubygems_test.go | 204 ++++++++++++ models/package.go | 39 ++- modules/context/context.go | 37 +-- modules/packages/rubygems/marshal.go | 311 ++++++++++++++++++ modules/packages/rubygems/marshal_test.go | 99 ++++++ modules/packages/rubygems/metadata.go | 215 ++++++++++++ modules/packages/rubygems/metadata_test.go | 89 +++++ options/locale/locale_en-US.ini | 6 + routers/api/v1/api.go | 12 + routers/api/v1/packages/nuget/nuget.go | 2 +- routers/api/v1/packages/rubygems/package.go | 75 +++++ routers/api/v1/packages/rubygems/rubygems.go | 229 +++++++++++++ routers/web/repo/packages.go | 3 + services/packages/packages.go | 7 +- templates/repo/packages/content/maven.tmpl | 14 +- templates/repo/packages/content/npm.tmpl | 14 +- templates/repo/packages/content/nuget.tmpl | 24 +- templates/repo/packages/content/pypi.tmpl | 7 +- templates/repo/packages/content/rubygems.tmpl | 76 +++++ templates/repo/packages/list.tmpl | 1 + templates/repo/packages/metadata/maven.tmpl | 7 +- templates/repo/packages/metadata/npm.tmpl | 4 +- templates/repo/packages/metadata/nuget.tmpl | 2 +- templates/repo/packages/metadata/pypi.tmpl | 7 +- .../repo/packages/metadata/rubygems.tmpl | 5 + templates/repo/packages/view.tmpl | 2 + 27 files changed, 1552 insertions(+), 61 deletions(-) create mode 100644 docs/content/doc/packages/rubygems.en-us.md create mode 100644 integrations/api_packages_rubygems_test.go create mode 100644 modules/packages/rubygems/marshal.go create mode 100644 modules/packages/rubygems/marshal_test.go create mode 100644 modules/packages/rubygems/metadata.go create mode 100644 modules/packages/rubygems/metadata_test.go create mode 100644 routers/api/v1/packages/rubygems/package.go create mode 100644 routers/api/v1/packages/rubygems/rubygems.go create mode 100644 templates/repo/packages/content/rubygems.tmpl create mode 100644 templates/repo/packages/metadata/rubygems.tmpl diff --git a/docs/content/doc/packages/rubygems.en-us.md b/docs/content/doc/packages/rubygems.en-us.md new file mode 100644 index 0000000000000..16e4125b9dac0 --- /dev/null +++ b/docs/content/doc/packages/rubygems.en-us.md @@ -0,0 +1,122 @@ +--- +date: "2021-07-20T00:00:00+00:00" +title: "RubyGems Packages Repository" +slug: "rubygems" +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "RubyGems" + weight: 60 + identifier: "rubygems" +--- + +# RubyGems Packages Repository + +Publish [RubyGems](https://guides.rubygems.org/) packages in your project’s Package Registry. + +**Table of Contents** + +{{< toc >}} + +## Requirements + +To work with the RubyGems package registry, you need to use the [gem](https://guides.rubygems.org/command-reference/) command line tool to consume and publish packages. + +## Configuring the package registry + +To register the project’s package registry edit the `~/.gem/credentials` file and add: + +```ini +--- +https://gitea.example.com/api/v1/repos/{owner}/{repository}/packages/rubygems: Bearer {token} +``` + +| Parameter | Description | +| ------------- | ----------- | +| `owner` | The owner of the repository. | +| `repository` | The name of the repository. | +| `token` | Your personal access token. | + +For example: + +```ini +--- +https://gitea.example.com/api/v1/repos/testuser/test-repository/packages/rubygems: Bearer 3bd626f84b01cd26b873931eace1e430a5773cc4 +``` + +## Publish a package + +Publish a package by running the following command: + +```shell +gem push --host {host} {package_file} +``` + +| Parameter | Description | +| -------------- | ----------- | +| `host` | URL to the package registry. | +| `package_file` | Path to the package `.gem` file. | + +For example: + +```shell +gem push --host https://gitea.example.com/api/v1/repos/testuser/test-repository/packages/rubygems test_package-1.0.0.gem +``` + +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 package from the package registry you can use [Bundler](https://bundler.io) or `gem`. + +### Bundler + +Add a new `source` block to your `Gemfile`: + +``` +source "https://gitea.example.com/api/v1/repos/{owner}/{repository}/packages/rubygems" do + gem "{package_name}" +end +``` + +| Parameter | Description | +| ----------------- | ----------- | +| `owner` | The owner of the repository. | +| `repository` | The name of the repository. | +| `package_name` | The package name. | + +For example: + +``` +source "https://gitea.example.com/api/v1/repos/testuser/test-repository/packages/rubygems" do + gem "test_package" +end +``` + +Afterwards run the following command: + +```shell +bundle install +``` + +### gem + +Execute the following command: + +```shell +gem install --host https://gitea.example.com/api/v1/repos/{owner}/{repository}/packages/rubygems {package_name} +``` + +| Parameter | Description | +| ----------------- | ----------- | +| `owner` | The owner of the repository. | +| `repository` | The name of the repository. | +| `package_name` | The package name. | + +For example: + +```shell +gem install --host https://gitea.example.com/api/v1/repos/testuser/test-repository/packages/rubygems test_package +``` \ No newline at end of file diff --git a/integrations/api_packages_rubygems_test.go b/integrations/api_packages_rubygems_test.go new file mode 100644 index 0000000000000..a81d8dc5cbf8d --- /dev/null +++ b/integrations/api_packages_rubygems_test.go @@ -0,0 +1,204 @@ +// Copyright 2021 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 ( + "bytes" + "encoding/base64" + "fmt" + "mime/multipart" + "net/http" + "testing" + + "code.gitea.io/gitea/models" + + "github.com/stretchr/testify/assert" +) + +func TestPackageRubyGems(t *testing.T) { + defer prepareTestEnv(t)() + repository := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 2}).(*models.Repository) + user := models.AssertExistsAndLoadBean(t, &models.User{ID: repository.OwnerID}).(*models.User) + + packageName := "gitea" + packageVersion := "1.0.5" + packageFilename := "gitea-1.0.5.gem" + + gemContent, _ := base64.StdEncoding.DecodeString(`bWV0YWRhdGEuZ3oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMDA0NDQAMDAwMDAw +MAAwMDAwMDAwADAwMDAwMDAxMDQxADE0MTEwNzcyMzY2ADAxMzQ0MQAgMAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1c3RhcgAwMHdoZWVsAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAd2hlZWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwADAwMDAw +MDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf +iwgA9vQjYQID1VVNb9QwEL37V5he9pRsmlJAFlQckCoOXAriQIUix5nNmsYf2JOqKwS/nYmz2d3Q +qqCCKpFdadfjmfdm5nmcLMv4k9DXm6Wrv4BCcQ5GiPcelF5pJVE7y6w0IHirESS7hhDJJu4I+jhu +Mc53Tsd5kZ8y30lcuWAEH2KY7HHtQhQs4+cJkwwuwNdeB6JhtbaNDoLTL1MQsFJrqQnr8jNrJJJH +WZTHWfEiK094UYj0zYvp4Z9YAx5sA1ZpSCS3M30zeWwo2bG60FvUBjIKJts2GwMW76r0Yr9NzjN3 +YhwsGX2Ozl4dpcWwvK9d43PQtDIv9igvHwSyIIwFmXHjqTqxLY8MPkCADmQk80p2EfZ6VbM6/ue6 +/1D0Bq7/qeA/zh6W82leHmhFWUHn/JbsEfT6q7QbiCpoj8l0QcEUFLmX6kq2wBEiMjBSd+Pwt7T5 +Ot0kuXYMbkD1KOuOBnWYb7hBsAP4bhlkFRqnqpWefMZ/pHCn6+WIFGq2dgY8EQq+RvRRLJcTyZJ1 +WhHqGPTu7QdmACXdJFLwb9+ZdxErbSPKrqsMxJhAWCJ1qaqRdtu6yktcT/STsamG0qp7rsa5EL/K +MBua30uw4ynzExqYWRJDfx8/kQWN3PwsDh2jYLr1W+pZcAmCs9splvnz/Flesqhbq21bXcGG/OLh ++2fv/JTF3hgZyCW9OaZjxoZjdnBGfgKpxZyJ1QYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGF0 +YS50YXIuZ3oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMDA0NDQAMDAwMDAwMAAw +MDAwMDAwADAwMDAwMDAwMjQyADE0MTEwNzcyMzY2ADAxMzM2MQAgMAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1c3RhcgAwMHdoZWVsAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAd2hlZWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwADAwMDAwMDAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfiwgA +9vQjYQID7M/NCsMgDABgz32KrA/QxersK/Q17ExXIcyhlr7+HLv1sJ02KPhBCPk5JOyn881nsl2c +xI+gRDRaC3zbZ8RBCamlxGHolTFlX11kLwDFH6wp21hO2RYi/rD3bb5/7iCubFOCMbBtABzNkIjn +bvGlAnisOUE7EnOALUR2p7b06e6aV4iqqqrquJ4AAAD//wMA+sA/NQAIAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGNoZWNr +c3Vtcy55YW1sLmd6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwNDQ0ADAwMDAwMDAAMDAw +MDAwMAAwMDAwMDAwMDQ1MAAxNDExMDc3MjM2NgAwMTQ2MTIAIDAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdXN0YXIAMDB3aGVlbAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAHdoZWVsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDAwMDAwMAAwMDAwMDAwAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4sIAPb0 +I2ECA2WQOa4UQAxE8znFXGCQ21vbPyMj5wRuL0Qk6EecnmZCyKyy9FSvXq/X4/u3ryj68Xg+f/Zn +VHzGlx+/P57qvU4XxWalBKftSXOgCjNYkdRycrC5Axem+W4HqS12PNEv7836jF9vnlHxwSyxKY+y +go0cPblyHzkrZ4HF1GSVhe7mOOoasXNk2fnbUxb+19Pp9tobD/QlJKMX7y204PREh6nQ5hG9Alw6 +x4TnmtA+aekGfm6wAseog2LSgpR4Q7cYnAH3K4qAQa6A6JCC1gpuY7P+9YxE5SZ+j0eVGbaBTwBQ +iIqRUyyzLCoFCBdYNWxniapTavD97blXTzFvgoVoAsKBAtlU48cdaOmeZDpwV01OtcGwjscfeUrY +B9QBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`) + + root := fmt.Sprintf("/api/v1/repos/%s/%s/packages/rubygems", user.Name, repository.Name) + + uploadFile := func(t *testing.T, expectedStatus int) { + req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/api/v1/gems", root), bytes.NewReader(gemContent)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, expectedStatus) + } + + t.Run("Upload", func(t *testing.T) { + defer PrintCurrentTest(t)() + + uploadFile(t, http.StatusCreated) + + ps, err := models.GetPackagesByRepositoryAndType(repository.ID, models.PackageRubyGems) + assert.NoError(t, err) + assert.Len(t, ps, 1) + assert.Equal(t, packageName, ps[0].Name) + assert.Equal(t, packageVersion, ps[0].Version) + + pfs, err := ps[0].GetFiles() + assert.NoError(t, err) + assert.Len(t, pfs, 1) + assert.Equal(t, packageFilename, pfs[0].Name) + assert.Equal(t, int64(4608), pfs[0].Size) + }) + + t.Run("UploadExists", func(t *testing.T) { + defer PrintCurrentTest(t)() + + uploadFile(t, http.StatusBadRequest) + }) + + t.Run("Download", func(t *testing.T) { + defer PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/gems/%s", root, packageFilename)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, gemContent, resp.Body.Bytes()) + }) + + t.Run("DownloadGemspec", func(t *testing.T) { + defer PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/quick/Marshal.4.8/%sspec.rz", root, packageFilename)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + b, _ := base64.StdEncoding.DecodeString(`eJxi4Si1EndPzbWyCi5ITc5My0xOLMnMz2M8zMIRLeGpxGWsZ6RnzGbF5hqSyempxJWeWZKayGbN +EBJqJQjWFZZaVJyZnxfN5qnEZahnoGcKkjTwVBJyB6lUKEhMzk5MTwULGngqcRaVJlWCONEMBp5K +DGAWSKc7zFhPJamg0qRK99TcYphehZLU4hKInFhGSUlBsZW+PtgZepn5+iDxECRzDUDGcfh6hoA4 +gAAAAP//MS06Gw==`) + assert.Equal(t, b, resp.Body.Bytes()) + }) + + t.Run("EnumeratePackages", func(t *testing.T) { + defer PrintCurrentTest(t)() + + enumeratePackages := func(t *testing.T, endpoint string, expectedContent []byte) { + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s", root, endpoint)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, expectedContent, resp.Body.Bytes()) + } + + b, _ := base64.StdEncoding.DecodeString(`H4sICAAAAAAA/3NwZWNzLjQuOABi4Yhmi+bwVOJKzyxJTWSzYnMNCbUSdE/NtbIKSy0qzszPi2bzVOIy1DPQM2WzZgjxVOIsKk2qBDEBAQAA///xOEYKOwAAAA==`) + enumeratePackages(t, "specs.4.8.gz", b) + b, _ = base64.StdEncoding.DecodeString(`H4sICAAAAAAA/2xhdGVzdF9zcGVjcy40LjgAYuGIZovm8FTiSs8sSU1ks2JzDQm1EnRPzbWyCkstKs7Mz4tm81TiMtQz0DNls2YI8VTiLCpNqgQxAQEAAP//8ThGCjsAAAA=`) + enumeratePackages(t, "latest_specs.4.8.gz", b) + b, _ = base64.StdEncoding.DecodeString(`H4sICAAAAAAA/3ByZXJlbGVhc2Vfc3BlY3MuNC44AGLhiGYABAAA//9snXr5BAAAAA==`) + enumeratePackages(t, "prerelease_specs.4.8.gz", b) + }) + + t.Run("Delete", func(t *testing.T) { + defer PrintCurrentTest(t)() + + body := bytes.Buffer{} + writer := multipart.NewWriter(&body) + writer.WriteField("gem_name", packageName) + writer.WriteField("version", packageVersion) + writer.Close() + + req := NewRequestWithBody(t, "DELETE", fmt.Sprintf("%s/api/v1/gems/yank", root), &body) + req.Header.Add("Content-Type", writer.FormDataContentType()) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusOK) + + ps, err := models.GetPackagesByRepositoryAndType(repository.ID, models.PackageRubyGems) + assert.NoError(t, err) + assert.Empty(t, ps) + }) +} diff --git a/models/package.go b/models/package.go index 45851373d3ebd..82cba7dce5590 100644 --- a/models/package.go +++ b/models/package.go @@ -18,11 +18,12 @@ type PackageType int // Note: new type must append to the end of list to maintain compatibility. const ( - PackageGeneric PackageType = iota - PackageNuGet // 1 - PackageNpm // 2 - PackageMaven // 3 - PackagePyPI // 4 + PackageGeneric PackageType = iota + PackageNuGet // 1 + PackageNpm // 2 + PackageMaven // 3 + PackagePyPI // 4 + PackageRubyGems // 5 ) func (pt PackageType) String() string { @@ -37,6 +38,8 @@ func (pt PackageType) String() string { return "Maven" case PackagePyPI: return "PyPI" + case PackageRubyGems: + return "RubyGems" } return "" } @@ -199,6 +202,8 @@ func (opts *PackageSearchOptions) toConds() builder.Cond { cond = cond.And(builder.Eq{"package.type": PackageMaven}) case "pypi": cond = cond.And(builder.Eq{"package.type": PackagePyPI}) + case "rubygems": + cond = cond.And(builder.Eq{"package.type": PackageRubyGems}) } if opts.Query != "" { @@ -212,7 +217,9 @@ func (opts *PackageSearchOptions) toConds() builder.Cond { func GetPackages(opts *PackageSearchOptions) ([]*Package, int64, error) { sess := x.Where(opts.toConds()) - sess = opts.Paginator.SetSessionPagination(sess) + if opts.Paginator != nil { + sess = opts.Paginator.SetSessionPagination(sess) + } packages := make([]*Package, 0, 10) count, err := sess.FindAndCount(&packages) @@ -228,7 +235,9 @@ func GetLatestPackagesGrouped(opts *PackageSearchOptions) ([]*Package, int64, er Table("package"). Join("left", "package p2", "package.repo_id = p2.repo_id AND package.type = p2.type AND package.lower_name = p2.lower_name AND package.version < p2.version") - sess = opts.Paginator.SetSessionPagination(sess) + if opts.Paginator != nil { + sess = opts.Paginator.SetSessionPagination(sess) + } packages := make([]*Package, 0, 10) count, err := sess.FindAndCount(&packages) @@ -282,6 +291,22 @@ func GetPackageByNameAndVersion(repositoryID int64, packageType PackageType, pac return p, nil } +// GetPackagesByFilename gets a repository packages by filename +func GetPackagesByFilename(repositoryID int64, packageType PackageType, packageFilename string) ([]*Package, error) { + var cond builder.Cond = builder.Eq{ + "package.repo_id": repositoryID, + "package.type": packageType, + "package_file.lower_name": strings.ToLower(packageFilename), + } + + packages := make([]*Package, 0, 10) + return packages, x. + Table("package"). + Where(cond). + Join("INNER", "package_file", "package.id = package_file.package_id"). + Find(&packages) +} + // TryInsertPackageFile inserts a package file func TryInsertPackageFile(pf *PackageFile) (*PackageFile, error) { sess := x.NewSession() diff --git a/modules/context/context.go b/modules/context/context.go index 64e9854d190ef..cf4755c5109d2 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -316,6 +316,18 @@ func (ctx *Context) HandleText(status int, title string) { ctx.PlainText(status, []byte(title)) } +// SetServeHeaders sets necessary content serve headers +func (ctx *Context) SetServeHeaders(filename string) { + ctx.Resp.Header().Set("Content-Description", "File Transfer") + ctx.Resp.Header().Set("Content-Type", "application/octet-stream") + ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+filename) + ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary") + ctx.Resp.Header().Set("Expires", "0") + ctx.Resp.Header().Set("Cache-Control", "must-revalidate") + ctx.Resp.Header().Set("Pragma", "public") + ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition") +} + // ServeContent serves content to http request func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interface{}) { modtime := time.Now() @@ -325,14 +337,7 @@ func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interfa modtime = v } } - ctx.Resp.Header().Set("Content-Description", "File Transfer") - ctx.Resp.Header().Set("Content-Type", "application/octet-stream") - ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+name) - ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary") - ctx.Resp.Header().Set("Expires", "0") - ctx.Resp.Header().Set("Cache-Control", "must-revalidate") - ctx.Resp.Header().Set("Pragma", "public") - ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition") + ctx.SetServeHeaders(name) http.ServeContent(ctx.Resp, ctx.Req, name, modtime, r) } @@ -353,25 +358,13 @@ func (ctx *Context) ServeFile(file string, names ...string) { } else { name = path.Base(file) } - ctx.Resp.Header().Set("Content-Description", "File Transfer") - ctx.Resp.Header().Set("Content-Type", "application/octet-stream") - ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+name) - ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary") - ctx.Resp.Header().Set("Expires", "0") - ctx.Resp.Header().Set("Cache-Control", "must-revalidate") - ctx.Resp.Header().Set("Pragma", "public") + ctx.SetServeHeaders(name) http.ServeFile(ctx.Resp, ctx.Req, file) } // ServeStream serves file via io stream func (ctx *Context) ServeStream(rd io.Reader, name string) { - ctx.Resp.Header().Set("Content-Description", "File Transfer") - ctx.Resp.Header().Set("Content-Type", "application/octet-stream") - ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+name) - ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary") - ctx.Resp.Header().Set("Expires", "0") - ctx.Resp.Header().Set("Cache-Control", "must-revalidate") - ctx.Resp.Header().Set("Pragma", "public") + ctx.SetServeHeaders(name) _, err := io.Copy(ctx.Resp, rd) if err != nil { ctx.ServerError("Download file failed", err) diff --git a/modules/packages/rubygems/marshal.go b/modules/packages/rubygems/marshal.go new file mode 100644 index 0000000000000..2c45042fa8c01 --- /dev/null +++ b/modules/packages/rubygems/marshal.go @@ -0,0 +1,311 @@ +// Copyright 2021 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 rubygems + +import ( + "bufio" + "bytes" + "errors" + "io" + "reflect" +) + +const ( + majorVersion = 4 + minorVersion = 8 + + typeNil = '0' + typeTrue = 'T' + typeFalse = 'F' + typeFixnum = 'i' + typeString = '"' + typeSymbol = ':' + typeSymbolLink = ';' + typeArray = '[' + typeIVar = 'I' + typeUserMarshal = 'U' + typeUserDef = 'u' + typeObject = 'o' +) + +var ( + // ErrUnsupportedType indicates an unsupported type + ErrUnsupportedType = errors.New("Type is unsupported") + // ErrInvalidIntRange indicates an invalid number range + ErrInvalidIntRange = errors.New("Number is not in valid range") +) + +// RubyUserMarshal is a Ruby object that has a marshal_load function. +type RubyUserMarshal struct { + Name string + Value interface{} +} + +// RubyUserDef is a Ruby object that has a _load function. +type RubyUserDef struct { + Name string + Value interface{} +} + +// RubyObject is a default Ruby object. +type RubyObject struct { + Name string + Member map[string]interface{} +} + +// MarshalEncoder mimics Rubys Marshal class. +// Note: Only supports types used by the RubyGems package registry. +type MarshalEncoder struct { + w *bufio.Writer + symbols map[string]int +} + +// NewMarshalEncoder creates a new MarshalEncoder +func NewMarshalEncoder(w io.Writer) *MarshalEncoder { + return &MarshalEncoder{ + w: bufio.NewWriter(w), + symbols: map[string]int{}, + } +} + +// Encode encodes the given type +func (e *MarshalEncoder) Encode(v interface{}) error { + if _, err := e.w.Write([]byte{majorVersion, minorVersion}); err != nil { + return err + } + + if err := e.marshal(v); err != nil { + return err + } + + return e.w.Flush() +} + +func (e *MarshalEncoder) marshal(v interface{}) error { + if v == nil { + return e.marshalNil() + } + + val := reflect.ValueOf(v) + typ := reflect.TypeOf(v) + + if typ.Kind() == reflect.Ptr { + val = val.Elem() + typ = typ.Elem() + } + + switch typ.Kind() { + case reflect.Bool: + return e.marshalBool(val.Bool()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: + return e.marshalInt(val.Int()) + case reflect.String: + return e.marshalString(val.String()) + case reflect.Slice, reflect.Array: + return e.marshalArray(val) + } + + switch typ.Name() { + case "RubyUserMarshal": + return e.marshalUserMarshal(val.Interface().(RubyUserMarshal)) + case "RubyUserDef": + return e.marshalUserDef(val.Interface().(RubyUserDef)) + case "RubyObject": + return e.marshalObject(val.Interface().(RubyObject)) + } + + return ErrUnsupportedType +} + +func (e *MarshalEncoder) marshalNil() error { + return e.w.WriteByte(typeNil) +} + +func (e *MarshalEncoder) marshalBool(b bool) error { + if b { + return e.w.WriteByte(typeTrue) + } + return e.w.WriteByte(typeFalse) +} + +func (e *MarshalEncoder) marshalInt(i int64) error { + if err := e.w.WriteByte(typeFixnum); err != nil { + return err + } + + return e.marshalIntInternal(i) +} + +func (e *MarshalEncoder) marshalIntInternal(i int64) error { + if i == 0 { + return e.w.WriteByte(0) + } else if 0 < i && i < 123 { + return e.w.WriteByte(byte(i + 5)) + } else if -124 < i && i <= -1 { + return e.w.WriteByte(byte(i - 5)) + } + + var len int + if 122 < i && i <= 0xff { + len = 1 + } else if 0xff < i && i <= 0xffff { + len = 2 + } else if 0xffff < i && i <= 0xffffff { + len = 3 + } else if 0xffffff < i && i <= 0x3fffffff { + len = 4 + } else if -0x100 <= i && i < -123 { + len = -1 + } else if -0x10000 <= i && i < -0x100 { + len = -2 + } else if -0x1000000 <= i && i < -0x100000 { + len = -3 + } else if -0x40000000 <= i && i < -0x1000000 { + len = -4 + } else { + return ErrInvalidIntRange + } + + if err := e.w.WriteByte(byte(len)); err != nil { + return err + } + if len < 0 { + len = -len + } + + for c := 0; c < len; c++ { + if err := e.w.WriteByte(byte(i >> uint(8*c) & 0xff)); err != nil { + return err + } + } + + return nil +} + +func (e *MarshalEncoder) marshalString(str string) error { + if err := e.w.WriteByte(typeIVar); err != nil { + return err + } + + if err := e.marshalRawString(str); err != nil { + return err + } + + if err := e.marshalIntInternal(1); err != nil { + return err + } + + if err := e.marshalSymbol("E"); err != nil { + return err + } + + return e.marshalBool(true) +} + +func (e *MarshalEncoder) marshalRawString(str string) error { + if err := e.w.WriteByte(typeString); err != nil { + return err + } + + if err := e.marshalIntInternal(int64(len(str))); err != nil { + return err + } + + _, err := e.w.WriteString(str) + return err +} + +func (e *MarshalEncoder) marshalSymbol(str string) error { + if index, ok := e.symbols[str]; ok { + if err := e.w.WriteByte(typeSymbolLink); err != nil { + return err + } + return e.marshalIntInternal(int64(index)) + } + + e.symbols[str] = len(e.symbols) + + if err := e.w.WriteByte(typeSymbol); err != nil { + return err + } + + if err := e.marshalIntInternal(int64(len(str))); err != nil { + return err + } + + _, err := e.w.WriteString(str) + return err +} + +func (e *MarshalEncoder) marshalArray(arr reflect.Value) error { + if err := e.w.WriteByte(typeArray); err != nil { + return err + } + + len := arr.Len() + + if err := e.marshalIntInternal(int64(len)); err != nil { + return err + } + + for i := 0; i < len; i++ { + if err := e.marshal(arr.Index(i).Interface()); err != nil { + return err + } + } + return nil +} + +func (e *MarshalEncoder) marshalUserMarshal(userMarshal RubyUserMarshal) error { + if err := e.w.WriteByte(typeUserMarshal); err != nil { + return err + } + + if err := e.marshalSymbol(userMarshal.Name); err != nil { + return err + } + + return e.marshal(userMarshal.Value) +} + +func (e *MarshalEncoder) marshalUserDef(userDef RubyUserDef) error { + var buf bytes.Buffer + if err := NewMarshalEncoder(&buf).Encode(userDef.Value); err != nil { + return err + } + + if err := e.w.WriteByte(typeUserDef); err != nil { + return err + } + if err := e.marshalSymbol(userDef.Name); err != nil { + return err + } + if err := e.marshalIntInternal(int64(buf.Len())); err != nil { + return err + } + _, err := e.w.Write(buf.Bytes()) + return err +} + +func (e *MarshalEncoder) marshalObject(obj RubyObject) error { + if err := e.w.WriteByte(typeObject); err != nil { + return err + } + if err := e.marshalSymbol(obj.Name); err != nil { + return err + } + if err := e.marshalIntInternal(int64(len(obj.Member))); err != nil { + return err + } + for k, v := range obj.Member { + if err := e.marshalSymbol(k); err != nil { + return err + } + if err := e.marshal(v); err != nil { + return err + } + } + return nil +} diff --git a/modules/packages/rubygems/marshal_test.go b/modules/packages/rubygems/marshal_test.go new file mode 100644 index 0000000000000..e5963ebcd632c --- /dev/null +++ b/modules/packages/rubygems/marshal_test.go @@ -0,0 +1,99 @@ +// Copyright 2021 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 rubygems + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMinimalEncoder(t *testing.T) { + cases := []struct { + Value interface{} + Expected []byte + Error error + }{ + { + Value: nil, + Expected: []byte{4, 8, 0x30}, + }, + { + Value: true, + Expected: []byte{4, 8, 'T'}, + }, + { + Value: false, + Expected: []byte{4, 8, 'F'}, + }, + { + Value: 0, + Expected: []byte{4, 8, 'i', 0}, + }, + { + Value: 1, + Expected: []byte{4, 8, 'i', 6}, + }, + { + Value: -1, + Expected: []byte{4, 8, 'i', 0xfa}, + }, + { + Value: 0x1fffffff, + Expected: []byte{4, 8, 'i', 4, 0xff, 0xff, 0xff, 0x1f}, + }, + { + Value: 0x41000000, + Error: ErrInvalidIntRange, + }, + { + Value: "test", + Expected: []byte{4, 8, 'I', '"', 9, 't', 'e', 's', 't', 6, ':', 6, 'E', 'T'}, + }, + { + Value: []int{1, 2}, + Expected: []byte{4, 8, '[', 7, 'i', 6, 'i', 7}, + }, + { + Value: &RubyUserMarshal{ + Name: "Test", + Value: 4, + }, + Expected: []byte{4, 8, 'U', ':', 9, 'T', 'e', 's', 't', 'i', 9}, + }, + { + Value: &RubyUserDef{ + Name: "Test", + Value: 4, + }, + Expected: []byte{4, 8, 'u', ':', 9, 'T', 'e', 's', 't', 9, 4, 8, 'i', 9}, + }, + { + Value: &RubyObject{ + Name: "Test", + Member: map[string]interface{}{ + "test": 4, + }, + }, + Expected: []byte{4, 8, 'o', ':', 9, 'T', 'e', 's', 't', 6, ':', 9, 't', 'e', 's', 't', 'i', 9}, + }, + { + Value: &struct { + Name string + }{ + "test", + }, + Error: ErrUnsupportedType, + }, + } + + for i, c := range cases { + var b bytes.Buffer + err := NewMarshalEncoder(&b).Encode(c.Value) + assert.ErrorIs(t, err, c.Error) + assert.Equal(t, c.Expected, b.Bytes(), "case %d", i) + } +} diff --git a/modules/packages/rubygems/metadata.go b/modules/packages/rubygems/metadata.go new file mode 100644 index 0000000000000..4fe90850bf2f7 --- /dev/null +++ b/modules/packages/rubygems/metadata.go @@ -0,0 +1,215 @@ +// Copyright 2021 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 rubygems + +import ( + "archive/tar" + "compress/gzip" + "errors" + "io" + "regexp" + "strings" + + "code.gitea.io/gitea/modules/validation" + + "gopkg.in/yaml.v2" +) + +var ( + // ErrMissingMetadataFile indicates a missing metadata.gz file + ErrMissingMetadataFile = errors.New("Metadata file is missing") + // ErrInvalidName indicates an invalid id in the metadata.gz file + ErrInvalidName = errors.New("Metadata file contains an invalid name") + // ErrInvalidVersion indicates an invalid version in the metadata.gz file + ErrInvalidVersion = errors.New("Metadata file contains an invalid version") +) + +var versionMatcher = regexp.MustCompile(`\A[0-9]+(?:\.[0-9a-zA-Z]+)*(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?\z`) + +// Metadata represents the metadata of a RubyGems package +type Metadata struct { + Name string `json:"-"` + Version string `json:"-"` + Platform string `json:"platform"` + Description string `json:"description"` + Summary string `json:"summary"` + Authors []string `json:"authors"` + Licenses []string `json:"licenses"` + RequiredRubyVersion []VersionRequirement `json:"required_ruby_version"` + RequiredRubygemsVersion []VersionRequirement `json:"required_rubygems_version"` + ProjectURL string `json:"project_url"` + RuntimeDependencies []Dependency `json:"runtime_dependencies"` + DevelopmentDependencies []Dependency `json:"development_dependencies"` +} + +// VersionRequirement represents a version restriction +type VersionRequirement struct { + Restriction string `json:"restriction"` + Version string `json:"version"` +} + +// Dependency represents a dependency of a RubyGems package +type Dependency struct { + Name string `json:"name"` + Version []VersionRequirement `json:"version"` +} + +type gemspec struct { + Name string `yaml:"name"` + Version struct { + Version string `yaml:"version"` + } `yaml:"version"` + Platform string `yaml:"platform"` + Authors []string `yaml:"authors"` + Autorequire interface{} `yaml:"autorequire"` + Bindir string `yaml:"bindir"` + CertChain []interface{} `yaml:"cert_chain"` + Date string `yaml:"date"` + Dependencies []struct { + Name string `yaml:"name"` + Requirement requirement `yaml:"requirement"` + Type string `yaml:"type"` + Prerelease bool `yaml:"prerelease"` + VersionRequirements requirement `yaml:"version_requirements"` + } `yaml:"dependencies"` + Description string `yaml:"description"` + Email string `yaml:"email"` + Executables []string `yaml:"executables"` + Extensions []interface{} `yaml:"extensions"` + ExtraRdocFiles []string `yaml:"extra_rdoc_files"` + Files []string `yaml:"files"` + Homepage string `yaml:"homepage"` + Licenses []string `yaml:"licenses"` + Metadata struct { + BugTrackerURI string `yaml:"bug_tracker_uri"` + ChangelogURI string `yaml:"changelog_uri"` + DocumentationURI string `yaml:"documentation_uri"` + SourceCodeURI string `yaml:"source_code_uri"` + } `yaml:"metadata"` + PostInstallMessage interface{} `yaml:"post_install_message"` + RdocOptions []interface{} `yaml:"rdoc_options"` + RequirePaths []string `yaml:"require_paths"` + RequiredRubyVersion requirement `yaml:"required_ruby_version"` + RequiredRubygemsVersion requirement `yaml:"required_rubygems_version"` + Requirements []interface{} `yaml:"requirements"` + RubygemsVersion string `yaml:"rubygems_version"` + SigningKey interface{} `yaml:"signing_key"` + SpecificationVersion int `yaml:"specification_version"` + Summary string `yaml:"summary"` + TestFiles []interface{} `yaml:"test_files"` +} + +type requirement struct { + Requirements [][]interface{} `yaml:"requirements"` +} + +// AsVersionRequirement converts into []VersionRequirement +func (r requirement) AsVersionRequirement() []VersionRequirement { + requirements := make([]VersionRequirement, 0, len(r.Requirements)) + for _, req := range r.Requirements { + if len(req) != 2 { + continue + } + restriction, ok := req[0].(string) + if !ok { + continue + } + vm, ok := req[1].(map[interface{}]interface{}) + if !ok { + continue + } + versionInt, ok := vm["version"] + if !ok { + continue + } + version, ok := versionInt.(string) + if !ok || version == "0" { + continue + } + + requirements = append(requirements, VersionRequirement{ + Restriction: restriction, + Version: version, + }) + } + return requirements +} + +// ParsePackageMetaData parses the metadata of a Gem package file +func ParsePackageMetaData(r io.Reader) (*Metadata, error) { + archive := tar.NewReader(r) + for { + hdr, err := archive.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if hdr.Name == "metadata.gz" { + return parseMetadataFile(archive) + } + } + + return nil, ErrMissingMetadataFile +} + +func parseMetadataFile(r io.Reader) (*Metadata, error) { + zr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + defer zr.Close() + + var spec gemspec + if err := yaml.NewDecoder(zr).Decode(&spec); err != nil { + return nil, err + } + + if len(spec.Name) == 0 || strings.Contains(spec.Name, "/") { + return nil, ErrInvalidName + } + + if !versionMatcher.MatchString(spec.Version.Version) { + return nil, ErrInvalidVersion + } + + if !validation.IsValidURL(spec.Homepage) { + spec.Homepage = "" + } + if !validation.IsValidURL(spec.Metadata.SourceCodeURI) { + spec.Metadata.SourceCodeURI = "" + } + + m := &Metadata{ + Name: spec.Name, + Version: spec.Version.Version, + Platform: spec.Platform, + Description: spec.Description, + Summary: spec.Summary, + Authors: spec.Authors, + Licenses: spec.Licenses, + ProjectURL: spec.Homepage, + RequiredRubyVersion: spec.RequiredRubyVersion.AsVersionRequirement(), + RequiredRubygemsVersion: spec.RequiredRubygemsVersion.AsVersionRequirement(), + DevelopmentDependencies: make([]Dependency, 0, 5), + RuntimeDependencies: make([]Dependency, 0, 5), + } + + for _, gemdep := range spec.Dependencies { + dep := Dependency{ + Name: gemdep.Name, + Version: gemdep.Requirement.AsVersionRequirement(), + } + if gemdep.Type == ":runtime" { + m.RuntimeDependencies = append(m.RuntimeDependencies, dep) + } else { + m.DevelopmentDependencies = append(m.DevelopmentDependencies, dep) + } + } + + return m, nil +} diff --git a/modules/packages/rubygems/metadata_test.go b/modules/packages/rubygems/metadata_test.go new file mode 100644 index 0000000000000..cf674f1306e4c --- /dev/null +++ b/modules/packages/rubygems/metadata_test.go @@ -0,0 +1,89 @@ +// Copyright 2021 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 rubygems + +import ( + "archive/tar" + "bytes" + "encoding/base64" + "io" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParsePackageMetaData(t *testing.T) { + createArchive := func(filename string, content []byte) io.Reader { + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + hdr := &tar.Header{ + Name: filename, + Mode: 0600, + Size: int64(len(content)), + } + tw.WriteHeader(hdr) + tw.Write(content) + tw.Close() + return &buf + } + + t.Run("MissingMetadataFile", func(t *testing.T) { + data := createArchive("dummy.txt", []byte{0}) + + m, err := ParsePackageMetaData(data) + assert.ErrorIs(t, err, ErrMissingMetadataFile) + assert.Nil(t, m) + }) + + t.Run("Valid", func(t *testing.T) { + content, _ := base64.StdEncoding.DecodeString("H4sICHC/I2EEAG1ldGFkYXRhAAEeAOH/bmFtZTogZwp2ZXJzaW9uOgogIHZlcnNpb246IDEKWw35Tx4AAAA=") + data := createArchive("metadata.gz", content) + + m, err := ParsePackageMetaData(data) + assert.NoError(t, err) + assert.NotNil(t, m) + }) +} + +func TestParseMetadataFile(t *testing.T) { + content, _ := base64.StdEncoding.DecodeString(`H4sIAMe7I2ECA9VVTW/UMBC9+1eYXvaUbJpSQBZUHJAqDlwK4kCFIseZzZrGH9iTqisEv52Js9nd +0KqggiqRXWnX45n3ZuZ5nCzL+JPQ15ulq7+AQnEORoj3HpReaSVRO8usNCB4qxEku4YQySbuCPo4 +bjHOd07HeZGfMt9JXLlgBB9imOxx7UIULOPnCZMMLsDXXgeiYbW2jQ6C0y9TELBSa6kJ6/IzaySS +R1mUx1nxIitPeFGI9M2L6eGfWAMebANWaUgktzN9M3lsKNmxutBb1AYyCibbNhsDFu+q9GK/Tc4z +d2IcLBl9js5eHaXFsLyvXeNz0LQyL/YoLx8EsiCMBZlx46k6sS2PDD5AgA5kJPNKdhH2elWzOv7n +uv9Q9Aau/6ngP84elvNpXh5oRVlB5/yW7BH0+qu0G4gqaI/JdEHBFBS5l+pKtsARIjIwUnfj8Le0 ++TrdJLl2DG5A9SjrjgZ1mG+4QbAD+G4ZZBUap6qVnnzGf6Rwp+vliBRqtnYGPBEKvkb0USyXE8mS +dVoR6hj07u0HZgAl3SRS8G/fmXcRK20jyq6rDMSYQFgidamqkXbbuspLXE/0k7GphtKqe67GuRC/ +yjAbmt9LsOMp8xMamFkSQ38fP5EFjdz8LA4do2C69VvqWXAJgrPbKZb58/xZXrKoW6ttW13Bhvzi +4ftn7/yUxd4YGcglvTmmY8aGY3ZwRn4CqcWcidUGAAA=`) + m, err := parseMetadataFile(bytes.NewReader(content)) + assert.NoError(t, err) + assert.NotNil(t, m) + + assert.Equal(t, "gitea", m.Name) + assert.Equal(t, "1.0.5", m.Version) + assert.Equal(t, "ruby", m.Platform) + assert.Equal(t, "Gitea package", m.Summary) + assert.Equal(t, "RubyGems package test", m.Description) + assert.Equal(t, []string{"Gitea"}, m.Authors) + assert.Equal(t, "https://gitea.io/", m.ProjectURL) + assert.Equal(t, []string{"MIT"}, m.Licenses) + assert.Empty(t, m.RequiredRubygemsVersion) + assert.Len(t, m.RequiredRubyVersion, 1) + assert.Equal(t, ">=", m.RequiredRubyVersion[0].Restriction) + assert.Equal(t, "2.3.0", m.RequiredRubyVersion[0].Version) + assert.Len(t, m.RuntimeDependencies, 1) + assert.Equal(t, "runtime-dep", m.RuntimeDependencies[0].Name) + assert.Len(t, m.RuntimeDependencies[0].Version, 2) + assert.Equal(t, ">=", m.RuntimeDependencies[0].Version[0].Restriction) + assert.Equal(t, "1.2.0", m.RuntimeDependencies[0].Version[0].Version) + assert.Equal(t, "<", m.RuntimeDependencies[0].Version[1].Restriction) + assert.Equal(t, "2.0", m.RuntimeDependencies[0].Version[1].Version) + assert.Len(t, m.DevelopmentDependencies, 1) + assert.Equal(t, "dev-dep", m.DevelopmentDependencies[0].Name) + assert.Len(t, m.DevelopmentDependencies[0].Version, 1) + assert.Equal(t, "~>", m.DevelopmentDependencies[0].Version[0].Restriction) + assert.Equal(t, "5.2", m.DevelopmentDependencies[0].Version[0].Version) +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 2cc59e579e391..67403122eb543 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1516,6 +1516,7 @@ packages.filter.package_type.all = All packages.published_by = Published %[1]s by %[3]s packages.installation = Installation packages.about = About this package +packages.requirements = Requirements packages.dependencies = Dependencies packages.details = Details packages.details.author = Author @@ -1542,6 +1543,11 @@ packages.npm.documentation = For more information on the npm registry, see the documentation. +packages.rubygems.use_1 = Install from the command line +packages.rubygems.use_2 = Install via Gemfile +packages.rubygems.required.ruby = Requires Ruby version +packages.rubygems.required.rubygems = Requires RubyGem version +packages.rubygems.documentation = For more information on the RubyGems registry, see the documentation. signing.will_sign = This commit will be signed with key '%s' signing.wont_sign.error = There was an error whilst checking if the commit could be signed diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index c49773063c518..46127f969a957 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -84,6 +84,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/packages/npm" "code.gitea.io/gitea/routers/api/v1/packages/nuget" "code.gitea.io/gitea/routers/api/v1/packages/pypi" + "code.gitea.io/gitea/routers/api/v1/packages/rubygems" "code.gitea.io/gitea/routers/api/v1/repo" "code.gitea.io/gitea/routers/api/v1/settings" _ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation @@ -1021,6 +1022,17 @@ func Routes() *web.Route { m.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile) m.Get("/simple/{id}", pypi.PackageMetadata) }, reqBasicAuth()) + m.Group("/rubygems", func() { + m.Get("/specs.4.8.gz", rubygems.EnumeratePackages) + m.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest) + m.Get("/prerelease_specs.4.8.gz", rubygems.EnumeratePackagesPreRelease) + m.Get("/quick/Marshal.4.8/{filename}", rubygems.ServePackageSpecification) + m.Get("/gems/{filename}", rubygems.DownloadPackageFile) + m.Group("/api/v1/gems", func() { + m.Post("/", rubygems.UploadPackageFile) + m.Delete("/yank", rubygems.DeletePackage) + }, reqRepoWriter(models.UnitTypePackages)) + }, reqToken()) m.Group("/{id}", func() { m.Get("/", repo.GetPackage) m.Delete("/", reqRepoWriter(models.UnitTypePackages), repo.DeletePackage) diff --git a/routers/api/v1/packages/nuget/nuget.go b/routers/api/v1/packages/nuget/nuget.go index 6bf1a92cbc673..beda583bb14d4 100644 --- a/routers/api/v1/packages/nuget/nuget.go +++ b/routers/api/v1/packages/nuget/nuget.go @@ -228,7 +228,7 @@ func DeletePackage(ctx *context.APIContext) { packageName := ctx.Params("id") packageVersion := ctx.Params("version") - err := package_service.DeletePackageByNameAndVersion(ctx.User, ctx.Repo.Repository, models.PackageGeneric, packageName, packageVersion) + err := package_service.DeletePackageByNameAndVersion(ctx.User, ctx.Repo.Repository, models.PackageNuGet, packageName, packageVersion) if err != nil { if err == models.ErrPackageNotExist { ctx.Error(http.StatusNotFound, "", err) diff --git a/routers/api/v1/packages/rubygems/package.go b/routers/api/v1/packages/rubygems/package.go new file mode 100644 index 0000000000000..cf978c621a45d --- /dev/null +++ b/routers/api/v1/packages/rubygems/package.go @@ -0,0 +1,75 @@ +// Copyright 2021 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 rubygems + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/json" + rubygems_module "code.gitea.io/gitea/modules/packages/rubygems" +) + +// Package represents a package with RubyGems metadata +type Package struct { + *models.Package + Metadata *rubygems_module.Metadata +} + +func intializePackages(packages []*models.Package) ([]*Package, error) { + pgs := make([]*Package, 0, len(packages)) + for _, p := range packages { + np, err := intializePackage(p) + if err != nil { + return nil, err + } + pgs = append(pgs, np) + } + return pgs, nil +} + +func intializePackage(p *models.Package) (*Package, error) { + var m *rubygems_module.Metadata + if err := json.Unmarshal([]byte(p.MetadataRaw), &m); err != nil { + return nil, err + } + if m == nil { + m = &rubygems_module.Metadata{} + } + + return &Package{ + Package: p, + Metadata: m, + }, nil +} + +// AsSpecification creates a Ruby Gem::Specification object used by ServePackageSpecification +func (p *Package) AsSpecification() *rubygems_module.RubyUserDef { + return &rubygems_module.RubyUserDef{ + Name: "Gem::Specification", + Value: []interface{}{ + "3.2.3", // @rubygems_version + 4, // @specification_version, + p.Name, + &rubygems_module.RubyUserMarshal{ + Name: "Gem::Version", + Value: []string{p.Version}, + }, + nil, // date + p.Metadata.Summary, // @summary + nil, // @required_ruby_version + nil, // @required_rubygems_version + p.Metadata.Platform, // @original_platform + []interface{}{}, // @dependencies + nil, // rubyforge_project + "", // @email + p.Metadata.Authors, + p.Metadata.Description, + p.Metadata.ProjectURL, + true, // has_rdoc + p.Metadata.Platform, // @new_platform + nil, + p.Metadata.Licenses, + }, + } +} diff --git a/routers/api/v1/packages/rubygems/rubygems.go b/routers/api/v1/packages/rubygems/rubygems.go new file mode 100644 index 0000000000000..5483dba9fbe0a --- /dev/null +++ b/routers/api/v1/packages/rubygems/rubygems.go @@ -0,0 +1,229 @@ +// Copyright 2021 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 rubygems + +import ( + "compress/gzip" + "compress/zlib" + "fmt" + "io" + "net/http" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + rubygems_module "code.gitea.io/gitea/modules/packages/rubygems" + "code.gitea.io/gitea/modules/util/filebuffer" + + package_service "code.gitea.io/gitea/services/packages" +) + +// EnumeratePackages serves the package list +func EnumeratePackages(ctx *context.APIContext) { + packages, err := models.GetPackagesByRepositoryAndType(ctx.Repo.Repository.ID, models.PackageRubyGems) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + enumeratePackages(ctx, "specs.4.8", packages) +} + +// EnumeratePackagesLatest serves the list of the lastest version of every package +func EnumeratePackagesLatest(ctx *context.APIContext) { + packages, _, err := models.GetLatestPackagesGrouped(&models.PackageSearchOptions{ + RepoID: ctx.Repo.Repository.ID, + Type: "rubygems", + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + enumeratePackages(ctx, "latest_specs.4.8", packages) +} + +// EnumeratePackagesPreRelease is not supported and serves an empty list +func EnumeratePackagesPreRelease(ctx *context.APIContext) { + enumeratePackages(ctx, "prerelease_specs.4.8", []*models.Package{}) +} + +func enumeratePackages(ctx *context.APIContext, filename string, packages []*models.Package) { + rubygemsPackages, err := intializePackages(packages) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + specs := make([]interface{}, 0, len(rubygemsPackages)) + for _, p := range rubygemsPackages { + specs = append(specs, []interface{}{ + p.Name, + &rubygems_module.RubyUserMarshal{ + Name: "Gem::Version", + Value: []string{p.Version}, + }, + p.Metadata.Platform, + }) + } + + ctx.SetServeHeaders(filename + ".gz") + + zw := gzip.NewWriter(ctx.Resp) + defer zw.Close() + + zw.Name = filename + + if err := rubygems_module.NewMarshalEncoder(zw).Encode(specs); err != nil { + ctx.ServerError("Download file failed", err) + } +} + +// ServePackageSpecification serves the compressed Gemspec file of a package +func ServePackageSpecification(ctx *context.APIContext) { + filename := ctx.Params("filename") + + if !strings.HasSuffix(filename, ".gemspec.rz") { + ctx.Error(http.StatusBadRequest, "", nil) + return + } + + packages, err := models.GetPackagesByFilename(ctx.Repo.Repository.ID, models.PackageRubyGems, filename[:len(filename)-10]+"gem") + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + if len(packages) != 1 { + ctx.Error(http.StatusNotFound, "", nil) + return + } + + p, err := intializePackage(packages[0]) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + ctx.SetServeHeaders(filename) + + zw := zlib.NewWriter(ctx.Resp) + defer zw.Close() + + spec := p.AsSpecification() + + if err := rubygems_module.NewMarshalEncoder(zw).Encode(spec); err != nil { + ctx.ServerError("Download file failed", err) + } +} + +// DownloadPackageFile serves the content of a package +func DownloadPackageFile(ctx *context.APIContext) { + filename := ctx.Params("filename") + + packages, err := models.GetPackagesByFilename(ctx.Repo.Repository.ID, models.PackageRubyGems, filename) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + if len(packages) != 1 { + ctx.Error(http.StatusNotFound, "", nil) + return + } + + s, pf, err := package_service.GetPackageFileStream(packages[0], filename) + if err != nil { + if err == models.ErrPackageFileNotExist { + ctx.Error(http.StatusNotFound, "", err) + return + } + ctx.Error(http.StatusInternalServerError, "", err) + return + } + defer s.Close() + + ctx.ServeStream(s, pf.Name) +} + +// UploadPackageFile adds a file to the package. If the package does not exist, it gets created. +func UploadPackageFile(ctx *context.APIContext) { + upload, close, err := ctx.UploadStream() + if err != nil { + ctx.Error(http.StatusBadRequest, "", err) + return + } + if close { + defer upload.Close() + } + + buf, err := filebuffer.CreateFromReader(upload, 32*1024*1024) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + defer buf.Close() + + meta, err := rubygems_module.ParsePackageMetaData(buf) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + if _, err := buf.Seek(0, io.SeekStart); err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + p, err := package_service.CreatePackage( + ctx.User, + ctx.Repo.Repository, + models.PackageRubyGems, + meta.Name, + meta.Version, + meta, + false, + ) + if err != nil { + if err == models.ErrDuplicatePackage { + ctx.Error(http.StatusBadRequest, "", err) + return + } + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + var filename string + if len(meta.Platform) == 0 || meta.Platform == "ruby" { + filename = strings.ToLower(fmt.Sprintf("%s-%s.gem", meta.Name, meta.Version)) + } else { + filename = strings.ToLower(fmt.Sprintf("%s-%s-%s.gem", meta.Name, meta.Version, meta.Platform)) + } + _, err = package_service.AddFileToPackage(p, filename, buf.Size(), buf) + if err != nil { + if err := models.DeletePackageByID(p.ID); err != nil { + log.Error("Error deleting package by id: %v", err) + } + ctx.Error(http.StatusInternalServerError, "", err) + return + } + + ctx.PlainText(http.StatusCreated, nil) +} + +// DeletePackage deletes a package +func DeletePackage(ctx *context.APIContext) { + packageName := ctx.FormString("gem_name") + packageVersion := ctx.FormString("version") + + err := package_service.DeletePackageByNameAndVersion(ctx.User, ctx.Repo.Repository, models.PackageRubyGems, packageName, packageVersion) + if err != nil { + if err == models.ErrPackageNotExist { + ctx.Error(http.StatusNotFound, "", err) + return + } + ctx.Error(http.StatusInternalServerError, "", "") + } +} diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index 49421e76d4be9..f66fff9c25332 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/packages/npm" "code.gitea.io/gitea/modules/packages/nuget" "code.gitea.io/gitea/modules/packages/pypi" + "code.gitea.io/gitea/modules/packages/rubygems" "code.gitea.io/gitea/modules/setting" package_service "code.gitea.io/gitea/services/packages" @@ -114,6 +115,8 @@ func ViewPackage(ctx *context.Context) { metadata = &maven.Metadata{} case models.PackagePyPI: metadata = &pypi.Metadata{} + case models.PackageRubyGems: + metadata = &rubygems.Metadata{} } if metadata != nil { if err := json.Unmarshal([]byte(p.MetadataRaw), &metadata); err != nil { diff --git a/services/packages/packages.go b/services/packages/packages.go index 032e8bf70afbf..9abfc5f3d6661 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -185,7 +185,7 @@ func GetFileStreamByPackageNameAndVersion(repository *models.Repository, package return nil, nil, err } - return getPackageFileStream(p, filename) + return GetPackageFileStream(p, filename) } // GetFileStreamByPackageID returns the content of the specific package file @@ -205,10 +205,11 @@ func GetFileStreamByPackageID(repository *models.Repository, packageID int64, fi return nil, nil, models.ErrPackageNotExist } - return getPackageFileStream(p, filename) + return GetPackageFileStream(p, filename) } -func getPackageFileStream(p *models.Package, filename string) (io.ReadCloser, *models.PackageFile, error) { +// GetPackageFileStream returns the cotent of the specific package file +func GetPackageFileStream(p *models.Package, filename string) (io.ReadCloser, *models.PackageFile, error) { pf, err := p.GetFileByName(filename) if err != nil { return nil, nil, err diff --git a/templates/repo/packages/content/maven.tmpl b/templates/repo/packages/content/maven.tmpl index 0a81641ac48bb..cfe721f9bc70e 100644 --- a/templates/repo/packages/content/maven.tmpl +++ b/templates/repo/packages/content/maven.tmpl @@ -51,10 +51,16 @@ {{if .Metadata.Dependencies}}

    {{.i18n.Tr "repo.packages.dependencies"}}

    -
    - {{range .Metadata.Dependencies}} -
    {{.GroupID}}:{{.ArtifactID}} ({{.Version}})
    - {{end}} +
    + {{range .Metadata.Dependencies}} +
    + {{svg "octicon-package-dependencies" 16 ""}} +
    +
    {{.GroupID}}:{{.ArtifactID}}
    +
    {{.Version}}
    +
    +
    + {{end}}
    {{end}} diff --git a/templates/repo/packages/content/npm.tmpl b/templates/repo/packages/content/npm.tmpl index a756fa2b19e55..47557feeb7be5 100644 --- a/templates/repo/packages/content/npm.tmpl +++ b/templates/repo/packages/content/npm.tmpl @@ -30,10 +30,16 @@ {{if .Metadata.Dependencies}}

    {{.i18n.Tr "repo.packages.dependencies"}}

    -
    - {{range $dependency, $version := .Metadata.Dependencies}} -
    {{$dependency}} ({{$version}})
    - {{end}} +
    + {{range $dependency, $version := .Metadata.Dependencies}} +
    + {{svg "octicon-package-dependencies" 16 ""}} +
    +
    {{$dependency}}
    +
    {{$version}}
    +
    +
    + {{end}}
    {{end}} diff --git a/templates/repo/packages/content/nuget.tmpl b/templates/repo/packages/content/nuget.tmpl index e4d931402149e..989338b12fcd6 100644 --- a/templates/repo/packages/content/nuget.tmpl +++ b/templates/repo/packages/content/nuget.tmpl @@ -25,14 +25,26 @@ {{if .Metadata.Dependencies}}

    {{.i18n.Tr "repo.packages.dependencies"}}

    - {{range $framework, $dependencies := .Metadata.Dependencies}} -
    -
    {{$framework}}
    - {{range $dependencies}} -
    {{.ID}} ({{.Version}})
    +
    + {{range $framework, $dependencies := .Metadata.Dependencies}} +
    +
    +
    {{$framework}}
    +
    + {{range $dependencies}} +
    + {{svg "octicon-package-dependencies" 16 ""}} +
    +
    {{.ID}}
    +
    {{.Version}}
    +
    +
    + {{end}} +
    +
    +
    {{end}}
    - {{end}}
    {{end}} {{end}} diff --git a/templates/repo/packages/content/pypi.tmpl b/templates/repo/packages/content/pypi.tmpl index 15d61d4b34492..5c8ed26df9b01 100644 --- a/templates/repo/packages/content/pypi.tmpl +++ b/templates/repo/packages/content/pypi.tmpl @@ -9,7 +9,6 @@
    {{.i18n.Tr "repo.packages.pypi.documentation" | Safe}}
    - {{if or .Metadata.Description .Metadata.LongDescription .Metadata.Summary}}

    {{.i18n.Tr "repo.packages.about"}}

    @@ -21,4 +20,10 @@ {{end}}
    {{end}} + {{if .Metadata.RequiresPython}} +

    {{.i18n.Tr "repo.packages.requirements"}}

    +
    + {{.i18n.Tr "repo.packages.pypi.requires"}}: {{.Metadata.RequiresPython}} +
    + {{end}} {{end}} diff --git a/templates/repo/packages/content/rubygems.tmpl b/templates/repo/packages/content/rubygems.tmpl new file mode 100644 index 0000000000000..85a0d307aa737 --- /dev/null +++ b/templates/repo/packages/content/rubygems.tmpl @@ -0,0 +1,76 @@ +{{if eq .Package.Type 5}} +

    {{.i18n.Tr "repo.packages.installation"}}

    +
    +
    + +
    + +
    + +
    + +
    +
    + {{.i18n.Tr "repo.packages.rubygems.documentation" | Safe}} +
    + {{if .Metadata.Description}} +

    {{.i18n.Tr "repo.packages.about"}}

    +
    + {{if .Metadata.Description}} + {{.Metadata.Description}} + {{end}} +
    + {{end}} + {{if or .Metadata.RequiredRubyVersion .Metadata.RequiredRubygemsVersion}} +

    {{.i18n.Tr "repo.packages.requirements"}}

    +
    + {{if .Metadata.RequiredRubyVersion}}

    {{.i18n.Tr "repo.packages.rubygems.required.ruby"}}: {{range $i, $v := .Metadata.RequiredRubyVersion}}{{if gt $i 0}}, {{end}}{{$v.Restriction}}{{$v.Version}}{{end}}

    {{end}} + {{if .Metadata.RequiredRubygemsVersion}}

    {{.i18n.Tr "repo.packages.rubygems.required.rubygems"}}: {{range $i, $v := .Metadata.RequiredRubygemsVersion}}{{if gt $i 0}}, {{end}}{{$v.Restriction}}{{$v.Version}}{{end}}

    {{end}} +
    + {{end}} + {{if or .Metadata.RuntimeDependencies .Metadata.DevelopmentDependencies}} +

    {{.i18n.Tr "repo.packages.dependencies"}}

    +
    +
    + {{if .Metadata.RuntimeDependencies}} +
    +
    +
    Runtime Dependencies
    +
    + {{range .Metadata.RuntimeDependencies}} +
    + {{svg "octicon-package-dependencies" 16 ""}} +
    +
    {{.Name}}
    +
    {{range $i, $v := .Version}}{{if gt $i 0}}, {{end}}{{$v.Restriction}}{{$v.Version}}{{end}}
    +
    +
    + {{end}} +
    +
    +
    + {{end}} + {{if .Metadata.DevelopmentDependencies}} +
    +
    +
    Development Dependencies
    +
    + {{range .Metadata.DevelopmentDependencies}} +
    + {{svg "octicon-package-dependencies" 16 ""}} +
    +
    {{.Name}}
    +
    {{range $i, $v := .Version}}{{if gt $i 0}}, {{end}}{{$v.Restriction}}{{$v.Version}}{{end}}
    +
    +
    + {{end}} +
    +
    +
    + {{end}} +
    +
    + {{end}} +{{end}} diff --git a/templates/repo/packages/list.tmpl b/templates/repo/packages/list.tmpl index 60338166afbd7..8ea24fd73fa98 100644 --- a/templates/repo/packages/list.tmpl +++ b/templates/repo/packages/list.tmpl @@ -27,6 +27,7 @@ npm Maven PyPI + RubyGems
    diff --git a/templates/repo/packages/metadata/maven.tmpl b/templates/repo/packages/metadata/maven.tmpl index b7015be23c34e..c9566b398f5cc 100644 --- a/templates/repo/packages/metadata/maven.tmpl +++ b/templates/repo/packages/metadata/maven.tmpl @@ -1,10 +1,5 @@ {{if eq .Package.Type 3}} {{if .Metadata.Name}}
    {{svg "octicon-note" 16 "mr-3"}} {{.Metadata.Name}}
    {{end}} {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} - {{if .Metadata.Licenses}} -
    {{.i18n.Tr "repo.packages.details.license"}}:
    - {{range .Metadata.Licenses}} -
    {{svg "octicon-law" 16 "mr-3"}} {{.}}
    - {{end}} - {{end}} + {{range .Metadata.Licenses}}
    {{svg "octicon-law" 16 "mr-3"}} {{.}}
    {{end}} {{end}} diff --git a/templates/repo/packages/metadata/npm.tmpl b/templates/repo/packages/metadata/npm.tmpl index 381aab099e0df..a05a84356f69a 100644 --- a/templates/repo/packages/metadata/npm.tmpl +++ b/templates/repo/packages/metadata/npm.tmpl @@ -1,5 +1,5 @@ {{if eq .Package.Type 2}} - {{if .Metadata.Author}}
    {{svg "octicon-person" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.author"}}: {{.Metadata.Author}}
    {{end}} + {{if .Metadata.Author}}
    {{svg "octicon-person" 16 "mr-3"}} {{.Metadata.Author}}
    {{end}} {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} - {{if .Metadata.License}}
    {{svg "octicon-law" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.license"}}: {{.Metadata.License}}
    {{end}} + {{if .Metadata.License}}
    {{svg "octicon-law" 16 "mr-3"}} {{.Metadata.License}}
    {{end}} {{end}} diff --git a/templates/repo/packages/metadata/nuget.tmpl b/templates/repo/packages/metadata/nuget.tmpl index 5e9c0da13d380..d9cb79ec388c3 100644 --- a/templates/repo/packages/metadata/nuget.tmpl +++ b/templates/repo/packages/metadata/nuget.tmpl @@ -1,4 +1,4 @@ {{if eq .Package.Type 1}} - {{if .Metadata.Authors}}
    {{svg "octicon-person" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.author"}}: {{.Metadata.Authors}}
    {{end}} + {{if .Metadata.Authors}}
    {{svg "octicon-person" 16 "mr-3"}} {{.Metadata.Authors}}
    {{end}} {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} {{end}} diff --git a/templates/repo/packages/metadata/pypi.tmpl b/templates/repo/packages/metadata/pypi.tmpl index 25ec8202bcc08..316f379255a5e 100644 --- a/templates/repo/packages/metadata/pypi.tmpl +++ b/templates/repo/packages/metadata/pypi.tmpl @@ -1,6 +1,5 @@ {{if eq .Package.Type 4}} - {{if .Metadata.Author}}
    {{svg "octicon-person" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.author"}}: {{.Metadata.Author}}
    {{end}} + {{if .Metadata.Author}}
    {{svg "octicon-person" 16 "mr-3"}} {{.Metadata.Author}}
    {{end}} {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} - {{if .Metadata.License}}
    {{svg "octicon-law" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.license"}}: {{.Metadata.License}}
    {{end}} - {{if .Metadata.RequiresPython}}
    {{.i18n.Tr "repo.packages.pypi.requires"}}: {{.Metadata.RequiresPython}}
    {{end}} -{{end}} + {{if .Metadata.License}}
    {{svg "octicon-law" 16 "mr-3"}} {{.Metadata.License}}
    {{end}} +{{end}} \ No newline at end of file diff --git a/templates/repo/packages/metadata/rubygems.tmpl b/templates/repo/packages/metadata/rubygems.tmpl new file mode 100644 index 0000000000000..3474204a80c03 --- /dev/null +++ b/templates/repo/packages/metadata/rubygems.tmpl @@ -0,0 +1,5 @@ +{{if eq .Package.Type 5}} + {{range .Metadata.Authors}}
    {{svg "octicon-person" 16 "mr-3"}} {{.}}
    {{end}} + {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} + {{range .Metadata.Licenses}}
    {{svg "octicon-law" 16 "mr-3"}} {{.}}
    {{end}} +{{end}} \ No newline at end of file diff --git a/templates/repo/packages/view.tmpl b/templates/repo/packages/view.tmpl index b55ce57339eec..239c183f62918 100644 --- a/templates/repo/packages/view.tmpl +++ b/templates/repo/packages/view.tmpl @@ -18,6 +18,7 @@ {{template "repo/packages/content/npm" .}} {{template "repo/packages/content/maven" .}} {{template "repo/packages/content/pypi" .}} + {{template "repo/packages/content/rubygems" .}}
    @@ -31,6 +32,7 @@ {{template "repo/packages/metadata/npm" .}} {{template "repo/packages/metadata/maven" .}} {{template "repo/packages/metadata/pypi" .}} + {{template "repo/packages/metadata/rubygems" .}}
    {{.i18n.Tr "repo.packages.assets"}} From c060e4a7d39a0a6bb93b7aae04aac9ac6a26924f Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 23 Aug 2021 20:10:00 +0000 Subject: [PATCH 036/130] Fix lint. --- templates/repo/packages/metadata/pypi.tmpl | 2 +- templates/repo/packages/metadata/rubygems.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/repo/packages/metadata/pypi.tmpl b/templates/repo/packages/metadata/pypi.tmpl index 316f379255a5e..c8215bffe73b0 100644 --- a/templates/repo/packages/metadata/pypi.tmpl +++ b/templates/repo/packages/metadata/pypi.tmpl @@ -2,4 +2,4 @@ {{if .Metadata.Author}}
    {{svg "octicon-person" 16 "mr-3"}} {{.Metadata.Author}}
    {{end}} {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} {{if .Metadata.License}}
    {{svg "octicon-law" 16 "mr-3"}} {{.Metadata.License}}
    {{end}} -{{end}} \ No newline at end of file +{{end}} diff --git a/templates/repo/packages/metadata/rubygems.tmpl b/templates/repo/packages/metadata/rubygems.tmpl index 3474204a80c03..f3937d7324ebf 100644 --- a/templates/repo/packages/metadata/rubygems.tmpl +++ b/templates/repo/packages/metadata/rubygems.tmpl @@ -2,4 +2,4 @@ {{range .Metadata.Authors}}
    {{svg "octicon-person" 16 "mr-3"}} {{.}}
    {{end}} {{if .Metadata.ProjectURL}}
    {{svg "octicon-link-external" 16 "mr-3"}} {{.i18n.Tr "repo.packages.details.project_site"}}
    {{end}} {{range .Metadata.Licenses}}
    {{svg "octicon-law" 16 "mr-3"}} {{.}}
    {{end}} -{{end}} \ No newline at end of file +{{end}} From cd1bc10fa35ea050de089554e6e03dde9fceee24 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Thu, 26 Aug 2021 18:06:00 +0200 Subject: [PATCH 037/130] Implemented io.Writer. --- modules/util/filebuffer/file_backed_buffer.go | 128 ++++++++++++------ 1 file changed, 90 insertions(+), 38 deletions(-) diff --git a/modules/util/filebuffer/file_backed_buffer.go b/modules/util/filebuffer/file_backed_buffer.go index 136f7d661e008..3339f373bf072 100644 --- a/modules/util/filebuffer/file_backed_buffer.go +++ b/modules/util/filebuffer/file_backed_buffer.go @@ -9,51 +9,94 @@ import ( "errors" "io" "io/ioutil" - "math" "os" ) +const maxInt = int(^uint(0) >> 1) // taken from bytes.Buffer + var ( // ErrInvalidMemorySize occurs if the memory size is not in a valid range ErrInvalidMemorySize = errors.New("Memory size must be greater 0 and lower math.MaxInt32") + // ErrWriteAfterRead occurs if Write is called after a read operation + ErrWriteAfterRead = errors.New("Write is unsupported after a read operation") ) -// FileBackedBuffer implements io.ReadSeekCloser and io.ReaderAt +type readAtSeeker interface { + io.ReadSeeker + io.ReaderAt +} + +// FileBackedBuffer uses a memory buffer with a fixed size. +// If more data is written a temporary file is used instead. +// It implements io.ReadWriteCloser, io.ReadSeekCloser and io.ReaderAt type FileBackedBuffer struct { - size int64 - buffer *bytes.Reader - tmpFile *os.File + maxMemorySize int64 + size int64 + buffer bytes.Buffer + file *os.File + reader readAtSeeker } -// CreateFromReader creates a file backed buffer which uses a buffer with a fixed memory size. -// If more data is available a temporary file is used to store the data. -func CreateFromReader(r io.Reader, maxMemorySize int) (*FileBackedBuffer, error) { - if maxMemorySize < 0 || maxMemorySize == math.MaxInt32 { +// New creates a file backed buffer with a specific maximum memory size +func New(maxMemorySize int) (*FileBackedBuffer, error) { + if maxMemorySize < 0 || maxMemorySize > maxInt { return nil, ErrInvalidMemorySize } - var buf bytes.Buffer - n, err := io.CopyN(&buf, r, int64(maxMemorySize+1)) - if err == io.EOF { - return &FileBackedBuffer{ - size: n, - buffer: bytes.NewReader(buf.Bytes()), - }, nil - } - file, err := ioutil.TempFile("", "buffer-") + return &FileBackedBuffer{ + maxMemorySize: int64(maxMemorySize), + }, nil +} + +// CreateFromReader creates a file backed buffer and copies the provided reader data into it. +func CreateFromReader(r io.Reader, maxMemorySize int) (*FileBackedBuffer, error) { + b, err := New(maxMemorySize) if err != nil { return nil, err } - n, err = io.Copy(file, io.MultiReader(&buf, r)) + _, err = io.Copy(b, r) if err != nil { return nil, err } - return &FileBackedBuffer{ - size: n, - tmpFile: file, - }, nil + return b, nil +} + +// Write implements io.Writer +func (b *FileBackedBuffer) Write(p []byte) (int, error) { + if b.reader != nil { + return 0, ErrWriteAfterRead + } + + var n int + var err error + + if b.file != nil { + n, err = b.file.Write(p) + } else { + if b.size+int64(len(p)) > b.maxMemorySize { + b.file, err = ioutil.TempFile("", "buffer-") + if err != nil { + return 0, err + } + + _, err = io.Copy(b.file, &b.buffer) + if err != nil { + return 0, err + } + + return b.Write(p) + } + + n, err = b.buffer.Write(p) + } + + if err != nil { + return n, err + } + b.size += int64(n) + return n, nil } // Size returns the byte size of the buffered data @@ -61,35 +104,44 @@ func (b *FileBackedBuffer) Size() int64 { return b.size } +func (b *FileBackedBuffer) switchToReader() { + if b.reader != nil { + return + } + + if b.file != nil { + b.reader = b.file + } else { + b.reader = bytes.NewReader(b.buffer.Bytes()) + } +} + // Read implements io.Reader func (b *FileBackedBuffer) Read(p []byte) (int, error) { - if b.tmpFile != nil { - return b.tmpFile.Read(p) - } - return b.buffer.Read(p) + b.switchToReader() + + return b.reader.Read(p) } // ReadAt implements io.ReaderAt func (b *FileBackedBuffer) ReadAt(p []byte, off int64) (int, error) { - if b.tmpFile != nil { - return b.tmpFile.ReadAt(p, off) - } - return b.buffer.ReadAt(p, off) + b.switchToReader() + + return b.reader.ReadAt(p, off) } // Seek implements io.Seeker func (b *FileBackedBuffer) Seek(offset int64, whence int) (int64, error) { - if b.tmpFile != nil { - return b.tmpFile.Seek(offset, whence) - } - return b.buffer.Seek(offset, whence) + b.switchToReader() + + return b.reader.Seek(offset, whence) } // Close implements io.Closer func (b *FileBackedBuffer) Close() error { - if b.tmpFile != nil { - err := b.tmpFile.Close() - os.Remove(b.tmpFile.Name()) + if b.file != nil { + err := b.file.Close() + os.Remove(b.file.Name()) return err } return nil From 2c9fa64828f38e452906739a6c42fa3185869aeb Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Wed, 1 Sep 2021 18:48:00 +0200 Subject: [PATCH 038/130] Added support for sha256/sha512 checksum files. --- routers/api/v1/packages/maven/maven.go | 66 ++++++++++++++++---------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/routers/api/v1/packages/maven/maven.go b/routers/api/v1/packages/maven/maven.go index c47b7700f3b30..67dfdcf807098 100644 --- a/routers/api/v1/packages/maven/maven.go +++ b/routers/api/v1/packages/maven/maven.go @@ -105,7 +105,7 @@ func servePackageFile(ctx *context.APIContext, params parameters) { filename := params.Filename ext := strings.ToLower(filepath.Ext(filename)) - if ext == ".sha1" || ext == ".md5" { + if isChecksumExtension(ext) { filename = filename[:len(filename)-len(ext)] } @@ -120,14 +120,22 @@ func servePackageFile(ctx *context.APIContext, params parameters) { return } - if ext == ".sha1" { - ctx.PlainText(http.StatusOK, []byte(pf.HashSHA1)) - return - } - if ext == ".md5" { - ctx.PlainText(http.StatusOK, []byte(pf.HashMD5)) + if isChecksumExtension(ext) { + var hash string + switch ext { + case ".sha512": + hash = pf.HashSHA512 + case ".sha256": + hash = pf.HashSHA256 + case ".sha1": + hash = pf.HashSHA1 + case ".md5": + hash = pf.HashMD5 + } + ctx.PlainText(http.StatusOK, []byte(hash)) return } + s, err := packages.NewContentStore().Get(p.ID, pf.ID) if err != nil { ctx.Error(http.StatusInternalServerError, "", err) @@ -180,29 +188,31 @@ func UploadPackageFile(ctx *context.APIContext) { ext := filepath.Ext(params.Filename) - if ext == ".sha1" || ext == ".md5" { - if ext == ".sha1" { - pf, err := p.GetFileByName(params.Filename[:len(params.Filename)-5]) - if err != nil { - if err == models.ErrPackageFileNotExist { - ctx.Error(http.StatusNotFound, "", "") - return - } - log.Error("GetFileByName: %v", err) - ctx.Error(http.StatusInternalServerError, "", err) + // Do not upload checksum files but compare the hashes. + if isChecksumExtension(ext) { + pf, err := p.GetFileByName(params.Filename[:len(params.Filename)-5]) + if err != nil { + if err == models.ErrPackageFileNotExist { + ctx.Error(http.StatusNotFound, "", "") return } + log.Error("GetFileByName: %v", err) + ctx.Error(http.StatusInternalServerError, "", err) + return + } - hash, err := ioutil.ReadAll(buf) - if err != nil { - ctx.Error(http.StatusInternalServerError, "", err) - return - } + hash, err := ioutil.ReadAll(buf) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } - if pf.HashSHA1 != string(hash) { - ctx.Error(http.StatusBadRequest, "", "hash mismatch") - return - } + if (ext == ".md5" && pf.HashMD5 != string(hash)) || + (ext == ".sha1" && pf.HashSHA1 != string(hash)) || + (ext == ".sha256" && pf.HashSHA256 != string(hash)) || + (ext == ".sha512" && pf.HashSHA512 != string(hash)) { + ctx.Error(http.StatusBadRequest, "", "hash mismatch") + return } ctx.PlainText(http.StatusOK, nil) @@ -246,6 +256,10 @@ func UploadPackageFile(ctx *context.APIContext) { ctx.PlainText(http.StatusCreated, nil) } +func isChecksumExtension(ext string) bool { + return ext == ".sha512" || ext == ".sha256" || ext == ".sha1" || ext == ".md5" +} + type parameters struct { GroupID string ArtifactID string From 41efc7c29ac720628c149dcf25d368f6742440af Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Thu, 2 Sep 2021 13:09:12 +0000 Subject: [PATCH 039/130] Improved maven-metadata.xml support. --- routers/api/v1/packages/maven/api.go | 15 ++- routers/api/v1/packages/maven/maven.go | 125 +++++++++++++---------- routers/api/v1/packages/maven/package.go | 5 +- 3 files changed, 89 insertions(+), 56 deletions(-) diff --git a/routers/api/v1/packages/maven/api.go b/routers/api/v1/packages/maven/api.go index e00c08ceeb8a0..60eeaff3f5372 100644 --- a/routers/api/v1/packages/maven/api.go +++ b/routers/api/v1/packages/maven/api.go @@ -6,6 +6,7 @@ package maven import ( "encoding/xml" + "strings" ) // MetadataResponse https://maven.apache.org/ref/3.2.5/maven-repository-metadata/repository-metadata.html @@ -13,24 +14,34 @@ type MetadataResponse struct { XMLName xml.Name `xml:"metadata"` GroupID string `xml:"groupId"` ArtifactID string `xml:"artifactId"` + Release string `xml:"versioning>release,omitempty"` Latest string `xml:"versioning>latest"` Version []string `xml:"versioning>versions>version"` } func createMetadataResponse(packages []*Package) *MetadataResponse { - sortedPackages := sortPackagesByVersionASC(packages) + sortedPackages := sortPackagesByCreationASC(packages) + + var release *Package versions := make([]string, 0, len(sortedPackages)) for _, p := range sortedPackages { + if !strings.HasSuffix(p.Version, "-SNAPSHOT") { + release = p + } versions = append(versions, p.Version) } latest := sortedPackages[len(sortedPackages)-1] - return &MetadataResponse{ + resp := &MetadataResponse{ GroupID: latest.Metadata.GroupID, ArtifactID: latest.Metadata.ArtifactID, Latest: latest.Version, Version: versions, } + if release != nil { + resp.Release = release.Version + } + return resp } diff --git a/routers/api/v1/packages/maven/maven.go b/routers/api/v1/packages/maven/maven.go index 67dfdcf807098..019511a476f9a 100644 --- a/routers/api/v1/packages/maven/maven.go +++ b/routers/api/v1/packages/maven/maven.go @@ -7,6 +7,8 @@ package maven import ( "crypto/md5" "crypto/sha1" + "crypto/sha256" + "crypto/sha512" "encoding/xml" "errors" "fmt" @@ -28,7 +30,13 @@ import ( package_service "code.gitea.io/gitea/services/packages" ) -const mavenMetadataFile = "maven-metadata.xml" +const ( + mavenMetadataFile = "maven-metadata.xml" + extensionMD5 = ".md5" + extensionSHA1 = ".sha1" + extensionSHA256 = ".sha256" + extensionSHA512 = ".sha512" +) var ( errInvalidParameters = errors.New("Request parameters are invalid") @@ -43,7 +51,7 @@ func DownloadPackageFile(ctx *context.APIContext) { return } - if params.IsMeta { + if params.IsMeta && params.Version == "" { serveMavenMetadata(ctx, params) } else { servePackageFile(ctx, params) @@ -51,46 +59,54 @@ func DownloadPackageFile(ctx *context.APIContext) { } func serveMavenMetadata(ctx *context.APIContext, params parameters) { - if params.Version == "" { - // /com/foo/project/maven-metadata.xml[.sha1/.md5] + // /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512] - packageName := params.GroupID + "-" + params.ArtifactID - packages, err := models.GetPackagesByName(ctx.Repo.Repository.ID, models.PackageMaven, packageName) - if err != nil { - ctx.Error(http.StatusInternalServerError, "", err) - return - } - if len(packages) == 0 { - ctx.Error(http.StatusNotFound, "", err) - return - } + packageName := params.GroupID + "-" + params.ArtifactID + packages, err := models.GetPackagesByName(ctx.Repo.Repository.ID, models.PackageMaven, packageName) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + if len(packages) == 0 { + ctx.Error(http.StatusNotFound, "", "") + return + } - mavenPackages, err := intializePackages(packages) - if err != nil { - ctx.Error(http.StatusInternalServerError, "", err) - return - } + mavenPackages, err := intializePackages(packages) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } - xmlMetadata, err := xml.Marshal(createMetadataResponse(mavenPackages)) - if err != nil { - ctx.Error(http.StatusInternalServerError, "", err) - return - } - xmlMetadataWithHeader := append([]byte(xml.Header), xmlMetadata...) - - switch strings.ToLower(filepath.Ext(params.Filename)) { - case ".sha1": - ctx.PlainText(http.StatusOK, []byte(fmt.Sprintf("%x", sha1.Sum(xmlMetadataWithHeader)))) - case ".md5": - ctx.PlainText(http.StatusOK, []byte(fmt.Sprintf("%x", md5.Sum(xmlMetadataWithHeader)))) - default: - ctx.PlainText(http.StatusOK, xmlMetadataWithHeader) - } - } else { - // /com/foo/project/1-SNAPSHOT/maven-metadata.xml[.sha1/.md5] + xmlMetadata, err := xml.Marshal(createMetadataResponse(mavenPackages)) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return + } + xmlMetadataWithHeader := append([]byte(xml.Header), xmlMetadata...) - ctx.Error(http.StatusNotFound, "", "") + ext := strings.ToLower(filepath.Ext(params.Filename)) + if isChecksumExtension(ext) { + var hash []byte + switch ext { + case extensionMD5: + tmp := md5.Sum(xmlMetadataWithHeader) + hash = tmp[:] + case extensionSHA1: + tmp := sha1.Sum(xmlMetadataWithHeader) + hash = tmp[:] + case extensionSHA256: + tmp := sha256.Sum256(xmlMetadataWithHeader) + hash = tmp[:] + case extensionSHA512: + tmp := sha512.Sum512(xmlMetadataWithHeader) + hash = tmp[:] + } + ctx.PlainText(http.StatusOK, []byte(fmt.Sprintf("%x", hash))) + return } + + ctx.PlainText(http.StatusOK, xmlMetadataWithHeader) } func servePackageFile(ctx *context.APIContext, params parameters) { @@ -123,14 +139,14 @@ func servePackageFile(ctx *context.APIContext, params parameters) { if isChecksumExtension(ext) { var hash string switch ext { - case ".sha512": - hash = pf.HashSHA512 - case ".sha256": - hash = pf.HashSHA256 - case ".sha1": - hash = pf.HashSHA1 - case ".md5": + case extensionMD5: hash = pf.HashMD5 + case extensionSHA1: + hash = pf.HashSHA1 + case extensionSHA256: + hash = pf.HashSHA256 + case extensionSHA512: + hash = pf.HashSHA512 } ctx.PlainText(http.StatusOK, []byte(hash)) return @@ -155,7 +171,8 @@ func UploadPackageFile(ctx *context.APIContext) { log.Trace("Parameters: %+v", params) - if params.IsMeta { + // Ignore the package index //maven-metadata.xml + if params.IsMeta && params.Version == "" { ctx.PlainText(http.StatusOK, nil) return } @@ -190,7 +207,7 @@ func UploadPackageFile(ctx *context.APIContext) { // Do not upload checksum files but compare the hashes. if isChecksumExtension(ext) { - pf, err := p.GetFileByName(params.Filename[:len(params.Filename)-5]) + pf, err := p.GetFileByName(params.Filename[:len(params.Filename)-len(ext)]) if err != nil { if err == models.ErrPackageFileNotExist { ctx.Error(http.StatusNotFound, "", "") @@ -207,10 +224,10 @@ func UploadPackageFile(ctx *context.APIContext) { return } - if (ext == ".md5" && pf.HashMD5 != string(hash)) || - (ext == ".sha1" && pf.HashSHA1 != string(hash)) || - (ext == ".sha256" && pf.HashSHA256 != string(hash)) || - (ext == ".sha512" && pf.HashSHA512 != string(hash)) { + if (ext == extensionMD5 && pf.HashMD5 != string(hash)) || + (ext == extensionSHA1 && pf.HashSHA1 != string(hash)) || + (ext == extensionSHA256 && pf.HashSHA256 != string(hash)) || + (ext == extensionSHA512 && pf.HashSHA512 != string(hash)) { ctx.Error(http.StatusBadRequest, "", "hash mismatch") return } @@ -257,7 +274,7 @@ func UploadPackageFile(ctx *context.APIContext) { } func isChecksumExtension(ext string) bool { - return ext == ".sha512" || ext == ".sha256" || ext == ".sha1" || ext == ".md5" + return ext == extensionMD5 || ext == extensionSHA1 || ext == extensionSHA256 || ext == extensionSHA512 } type parameters struct { @@ -275,7 +292,11 @@ func extractPathParameters(ctx *context.APIContext) (parameters, error) { Filename: parts[len(parts)-1], } - p.IsMeta = p.Filename == mavenMetadataFile || p.Filename == mavenMetadataFile+".sha1" || p.Filename == mavenMetadataFile+".md5" + p.IsMeta = p.Filename == mavenMetadataFile || + p.Filename == mavenMetadataFile+extensionMD5 || + p.Filename == mavenMetadataFile+extensionSHA1 || + p.Filename == mavenMetadataFile+extensionSHA256 || + p.Filename == mavenMetadataFile+extensionSHA512 parts = parts[:len(parts)-1] if len(parts) == 0 { diff --git a/routers/api/v1/packages/maven/package.go b/routers/api/v1/packages/maven/package.go index 6ac7000bd33cb..02dc69e5dffa4 100644 --- a/routers/api/v1/packages/maven/package.go +++ b/routers/api/v1/packages/maven/package.go @@ -46,12 +46,13 @@ func intializePackage(p *models.Package) (*Package, error) { }, nil } -func sortPackagesByVersionASC(packages []*Package) []*Package { +// Maven and Gradle order packages by their creation timestamp and not by their version string +func sortPackagesByCreationASC(packages []*Package) []*Package { sortedPackages := make([]*Package, len(packages)) copy(sortedPackages, packages) sort.Slice(sortedPackages, func(i, j int) bool { - return sortedPackages[i].Version < sortedPackages[j].Version + return sortedPackages[i].CreatedUnix < sortedPackages[j].CreatedUnix }) return sortedPackages From 1b666dc11d714730234422c9b1e48191757c5239 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 6 Sep 2021 18:24:04 +0000 Subject: [PATCH 040/130] Added support for symbol package uploads. --- modules/packages/nuget/metadata.go | 27 +++++- modules/packages/nuget/metadata_test.go | 64 ++++++++++---- routers/api/v1/api.go | 1 + routers/api/v1/packages/nuget/api.go | 1 + routers/api/v1/packages/nuget/nuget.go | 107 +++++++++++++++++++----- 5 files changed, 162 insertions(+), 38 deletions(-) diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index 7d2c3e293cbc5..2ac99e3fda121 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -29,12 +29,23 @@ var ( ErrNuspecInvalidVersion = errors.New("Nuspec file contains an invalid version") ) +// PackageType specifies the package type the metadata describes +type PackageType int + +const ( + // DependencyPackage represents a package (*.nupkg) + DependencyPackage PackageType = iota + 1 + // SymbolsPackage represents a symbol package (*.snupkg) + SymbolsPackage +) + var idmatch = regexp.MustCompile(`\A\w+(?:[.-]\w+)*\z`) const maxNuspecFileSize = 3 * 1024 * 1024 // Metadata represents the metadata of a Nuget package type Metadata struct { + PackageType PackageType `json:"-"` ID string `json:"-"` Version string `json:"-"` Description string `json:"description"` @@ -60,7 +71,12 @@ type nuspecPackage struct { ProjectURL string `xml:"projectUrl"` Description string `xml:"description"` ReleaseNotes string `xml:"releaseNotes"` - Repository struct { + PackageTypes struct { + PackageType []struct { + Name string `xml:"name,attr"` + } `xml:"packageType"` + } `xml:"packageTypes"` + Repository struct { URL string `xml:"url,attr"` } `xml:"repository"` Dependencies struct { @@ -123,7 +139,16 @@ func ParseNuspecMetaData(r io.Reader) (*Metadata, error) { p.Metadata.ProjectURL = "" } + packageType := DependencyPackage + for _, pt := range p.Metadata.PackageTypes.PackageType { + if pt.Name == "SymbolsPackage" { + packageType = SymbolsPackage + break + } + } + m := &Metadata{ + PackageType: packageType, ID: p.Metadata.ID, Version: v.String(), Description: p.Metadata.Description, diff --git a/modules/packages/nuget/metadata_test.go b/modules/packages/nuget/metadata_test.go index 40a00a190e4f9..e4418c3cd2e84 100644 --- a/modules/packages/nuget/metadata_test.go +++ b/modules/packages/nuget/metadata_test.go @@ -45,6 +45,21 @@ const nuspecContent = ` ` +const symbolsNuspecContent = ` + + + ` + id + ` + ` + semver + ` + ` + description + ` + + + + + + + +` + func TestParsePackageMetaData(t *testing.T) { createArchive := func(name, content string) []byte { var buf bytes.Buffer @@ -113,21 +128,36 @@ func TestParsePackageMetaData(t *testing.T) { } func TestParseNuspecMetaData(t *testing.T) { - m, err := ParseNuspecMetaData(strings.NewReader(nuspecContent)) - assert.NoError(t, err) - assert.NotNil(t, m) - - assert.Equal(t, id, m.ID) - assert.Equal(t, semver, m.Version) - assert.Equal(t, authors, m.Authors) - assert.Equal(t, projectURL, m.ProjectURL) - assert.Equal(t, description, m.Description) - assert.Equal(t, releaseNotes, m.ReleaseNotes) - assert.Equal(t, repositoryURL, m.RepositoryURL) - assert.Len(t, m.Dependencies, 1) - assert.Contains(t, m.Dependencies, targetFramework) - deps := m.Dependencies[targetFramework] - assert.Len(t, deps, 1) - assert.Equal(t, dependencyID, deps[0].ID) - assert.Equal(t, dependencyVersion, deps[0].Version) + t.Run("Dependency Package", func(t *testing.T) { + m, err := ParseNuspecMetaData(strings.NewReader(nuspecContent)) + assert.NoError(t, err) + assert.NotNil(t, m) + assert.Equal(t, DependencyPackage, m.PackageType) + + assert.Equal(t, id, m.ID) + assert.Equal(t, semver, m.Version) + assert.Equal(t, authors, m.Authors) + assert.Equal(t, projectURL, m.ProjectURL) + assert.Equal(t, description, m.Description) + assert.Equal(t, releaseNotes, m.ReleaseNotes) + assert.Equal(t, repositoryURL, m.RepositoryURL) + assert.Len(t, m.Dependencies, 1) + assert.Contains(t, m.Dependencies, targetFramework) + deps := m.Dependencies[targetFramework] + assert.Len(t, deps, 1) + assert.Equal(t, dependencyID, deps[0].ID) + assert.Equal(t, dependencyVersion, deps[0].Version) + }) + + t.Run("Symbols Package", func(t *testing.T) { + m, err := ParseNuspecMetaData(strings.NewReader(symbolsNuspecContent)) + assert.NoError(t, err) + assert.NotNil(t, m) + assert.Equal(t, SymbolsPackage, m.PackageType) + + assert.Equal(t, id, m.ID) + assert.Equal(t, semver, m.Version) + assert.Equal(t, description, m.Description) + assert.Empty(t, m.Dependencies) + }) } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 46127f969a957..90c0daa77c206 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1009,6 +1009,7 @@ func Routes() *web.Route { m.Get("/{filename}", nuget.DownloadPackageFile) }) }) + m.Put("/symbolpackage", nuget.UploadSymbolPackage) }, reqBasicAuth()) m.Group("/npm", func() { m.Group("/{id}", func() { diff --git a/routers/api/v1/packages/nuget/api.go b/routers/api/v1/packages/nuget/api.go index 1e8c372a54a42..18515283ffb79 100644 --- a/routers/api/v1/packages/nuget/api.go +++ b/routers/api/v1/packages/nuget/api.go @@ -36,6 +36,7 @@ func createServiceIndexResponse(root string) *ServiceIndexResponse { {ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-rc"}, {ID: root + "/package", Type: "PackageBaseAddress/3.0.0"}, {ID: root, Type: "PackagePublish/2.0.0"}, + {ID: root + "/symbolpackage", Type: "SymbolPackagePublish/4.9.0"}, }, } } diff --git a/routers/api/v1/packages/nuget/nuget.go b/routers/api/v1/packages/nuget/nuget.go index beda583bb14d4..47ee0ce1d1421 100644 --- a/routers/api/v1/packages/nuget/nuget.go +++ b/routers/api/v1/packages/nuget/nuget.go @@ -165,43 +165,74 @@ func DownloadPackageFile(ctx *context.APIContext) { // UploadPackage creates a new package with the metadata contained in the uploaded nupgk file // https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#push-a-package func UploadPackage(ctx *context.APIContext) { - upload, close, err := ctx.UploadStream() - if err != nil { - ctx.Error(http.StatusBadRequest, "", err) + meta, buf, closables := processUploadedFile(ctx, nuget_module.DependencyPackage) + defer func() { + for _, c := range closables { + c.Close() + } + }() + if meta == nil { return } - if close { - defer upload.Close() - } - buf, err := filebuffer.CreateFromReader(upload, 32*1024*1024) + p, err := package_service.CreatePackage( + ctx.User, + ctx.Repo.Repository, + models.PackageNuGet, + meta.ID, + meta.Version, + meta, + false, + ) if err != nil { + if err == models.ErrDuplicatePackage { + ctx.Error(http.StatusBadRequest, "", err) + return + } ctx.Error(http.StatusInternalServerError, "", err) return } - defer buf.Close() - meta, err := nuget_module.ParsePackageMetaData(buf, buf.Size()) + filename := strings.ToLower(fmt.Sprintf("%s.%s.nupkg", meta.ID, meta.Version)) + _, err = package_service.AddFileToPackage(p, filename, buf.Size(), buf) if err != nil { + if err := models.DeletePackageByID(p.ID); err != nil { + log.Error("Error deleting package by id: %v", err) + } ctx.Error(http.StatusInternalServerError, "", err) return } - if _, err := buf.Seek(0, io.SeekStart); err != nil { + + ctx.PlainText(http.StatusCreated, nil) +} + +// UploadSymbolPackage adds a symbol package to an existing package +// https://docs.microsoft.com/en-us/nuget/api/symbol-package-publish-resource +func UploadSymbolPackage(ctx *context.APIContext) { + meta, buf, closables := processUploadedFile(ctx, nuget_module.SymbolsPackage) + defer func() { + for _, c := range closables { + c.Close() + } + }() + if meta == nil { + return + } + + p, err := models.GetPackageByNameAndVersion(ctx.Repo.Repository.ID, models.PackageNuGet, meta.ID, meta.Version) + if err != nil { + if err == models.ErrPackageNotExist { + ctx.Error(http.StatusNotFound, "", err) + return + } ctx.Error(http.StatusInternalServerError, "", err) return } - p, err := package_service.CreatePackage( - ctx.User, - ctx.Repo.Repository, - models.PackageNuGet, - meta.ID, - meta.Version, - meta, - false, - ) + filename := strings.ToLower(fmt.Sprintf("%s.%s.snupkg", meta.ID, meta.Version)) + _, err = package_service.AddFileToPackage(p, filename, buf.Size(), buf) if err != nil { - if err == models.ErrDuplicatePackage { + if err == models.ErrDuplicatePackageFile { ctx.Error(http.StatusBadRequest, "", err) return } @@ -222,6 +253,42 @@ func UploadPackage(ctx *context.APIContext) { ctx.PlainText(http.StatusCreated, nil) } +func processUploadedFile(ctx *context.APIContext, expectedType nuget_module.PackageType) (*nuget_module.Metadata, *filebuffer.FileBackedBuffer, []io.Closer) { + closables := make([]io.Closer, 0, 2) + + upload, close, err := ctx.UploadStream() + if err != nil { + ctx.Error(http.StatusBadRequest, "", err) + return nil, nil, closables + } + + if close { + closables = append(closables, upload) + } + + buf, err := filebuffer.CreateFromReader(upload, 32*1024*1024) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return nil, nil, closables + } + closables = append(closables, buf) + + meta, err := nuget_module.ParsePackageMetaData(buf, buf.Size()) + if err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return nil, nil, closables + } + if meta.PackageType != expectedType { + ctx.Error(http.StatusBadRequest, "", err) + return nil, nil, closables + } + if _, err := buf.Seek(0, io.SeekStart); err != nil { + ctx.Error(http.StatusInternalServerError, "", err) + return nil, nil, closables + } + return meta, buf, closables +} + // DeletePackage hard deletes the package // https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#delete-a-package func DeletePackage(ctx *context.APIContext) { From 3e462afd87cc2dd3da5e74c05bf7e5017aac8edf Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 6 Sep 2021 19:03:50 +0000 Subject: [PATCH 041/130] Added tests. --- integrations/api_packages_nuget_test.go | 97 +++++++++++++++++++------ routers/api/v1/packages/nuget/nuget.go | 10 --- 2 files changed, 75 insertions(+), 32 deletions(-) diff --git a/integrations/api_packages_nuget_test.go b/integrations/api_packages_nuget_test.go index 84e1fc1b7e0f3..b128264343d95 100644 --- a/integrations/api_packages_nuget_test.go +++ b/integrations/api_packages_nuget_test.go @@ -8,6 +8,7 @@ import ( "archive/zip" "bytes" "fmt" + "io" "net/http" "testing" @@ -82,31 +83,77 @@ func TestPackageNuGet(t *testing.T) { }) t.Run("Upload", func(t *testing.T) { - defer PrintCurrentTest(t)() + t.Run("DependencyPackage", func(t *testing.T) { + defer PrintCurrentTest(t)() - req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)) - req = AddBasicAuthHeader(req, user.Name) - MakeRequest(t, req, http.StatusCreated) + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusCreated) - ps, err := models.GetPackagesByRepositoryAndType(repository.ID, models.PackageNuGet) - assert.NoError(t, err) - assert.Len(t, ps, 1) - assert.Equal(t, packageName, ps[0].Name) - assert.Equal(t, packageVersion, ps[0].Version) + ps, err := models.GetPackagesByRepositoryAndType(repository.ID, models.PackageNuGet) + assert.NoError(t, err) + assert.Len(t, ps, 1) + assert.Equal(t, packageName, ps[0].Name) + assert.Equal(t, packageVersion, ps[0].Version) - pfs, err := ps[0].GetFiles() - assert.NoError(t, err) - assert.Len(t, pfs, 1) - assert.Equal(t, fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion), pfs[0].Name) - assert.Equal(t, int64(len(content)), pfs[0].Size) - }) + pfs, err := ps[0].GetFiles() + assert.NoError(t, err) + assert.Len(t, pfs, 1) + assert.Equal(t, fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion), pfs[0].Name) + assert.Equal(t, int64(len(content)), pfs[0].Size) - t.Run("UploadExists", func(t *testing.T) { - defer PrintCurrentTest(t)() + req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusBadRequest) + }) - req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)) - req = AddBasicAuthHeader(req, user.Name) - MakeRequest(t, req, http.StatusBadRequest) + t.Run("SymbolPackage", func(t *testing.T) { + defer PrintCurrentTest(t)() + + createPackage := func(id, packageType string) io.Reader { + var buf bytes.Buffer + archive := zip.NewWriter(&buf) + w, _ := archive.Create("package.nuspec") + w.Write([]byte(` + + + ` + id + ` + ` + packageVersion + ` + ` + packageAuthors + ` + ` + packageDescription + ` + + + `)) + archive.Close() + return &buf + } + + req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage("unknown-package", "SymbolsPackage")) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusNotFound) + + req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage(packageName, "DummyPackage")) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusBadRequest) + + req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage(packageName, "SymbolsPackage")) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusCreated) + + ps, err := models.GetPackagesByRepositoryAndType(repository.ID, models.PackageNuGet) + assert.NoError(t, err) + assert.Len(t, ps, 1) + + pfs, err := ps[0].GetFiles() + assert.NoError(t, err) + assert.Len(t, pfs, 2) + assert.Equal(t, fmt.Sprintf("%s.%s.snupkg", packageName, packageVersion), pfs[1].Name) + assert.Equal(t, int64(368), pfs[1].Size) + + req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage(packageName, "SymbolsPackage")) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusBadRequest) + }) }) t.Run("Download", func(t *testing.T) { @@ -117,6 +164,10 @@ func TestPackageNuGet(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) assert.Equal(t, content, resp.Body.Bytes()) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.snupkg", url, packageName, packageVersion, packageName, packageVersion)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusOK) }) t.Run("SearchService", func(t *testing.T) { @@ -150,8 +201,6 @@ func TestPackageNuGet(t *testing.T) { }) t.Run("RegistrationService", func(t *testing.T) { - defer PrintCurrentTest(t)() - indexURL := fmt.Sprintf("%s%s/registration/%s/index.json", setting.AppURL, url[1:], packageName) leafURL := fmt.Sprintf("%s%s/registration/%s/%s.json", setting.AppURL, url[1:], packageName, packageVersion) contentURL := fmt.Sprintf("%s%s/package/%s/%s/%s.%s.nupkg", setting.AppURL, url[1:], packageName, packageVersion, packageName, packageVersion) @@ -230,6 +279,10 @@ func TestPackageNuGet(t *testing.T) { req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", url, packageName, packageVersion, packageName, packageVersion)) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusNotFound) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.snupkg", url, packageName, packageVersion, packageName, packageVersion)) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusNotFound) }) t.Run("DeleteNotExists", func(t *testing.T) { diff --git a/routers/api/v1/packages/nuget/nuget.go b/routers/api/v1/packages/nuget/nuget.go index 47ee0ce1d1421..01da9ad1ea6c2 100644 --- a/routers/api/v1/packages/nuget/nuget.go +++ b/routers/api/v1/packages/nuget/nuget.go @@ -240,16 +240,6 @@ func UploadSymbolPackage(ctx *context.APIContext) { return } - filename := strings.ToLower(fmt.Sprintf("%s.%s.nupkg", meta.ID, meta.Version)) - _, err = package_service.AddFileToPackage(p, filename, buf.Size(), buf) - if err != nil { - if err := models.DeletePackageByID(p.ID); err != nil { - log.Error("Error deleting package by id: %v", err) - } - ctx.Error(http.StatusInternalServerError, "", err) - return - } - ctx.PlainText(http.StatusCreated, nil) } From 39a4115b53f88bd2414cb85cbf7cbff076236b0d Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 6 Sep 2021 21:01:47 +0000 Subject: [PATCH 042/130] Added overview docs. --- docs/content/doc/packages/overview.en-us.md | 67 +++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/content/doc/packages/overview.en-us.md diff --git a/docs/content/doc/packages/overview.en-us.md b/docs/content/doc/packages/overview.en-us.md new file mode 100644 index 0000000000000..3995dc86396e4 --- /dev/null +++ b/docs/content/doc/packages/overview.en-us.md @@ -0,0 +1,67 @@ +--- +date: "2021-07-20T00:00:00+00:00" +title: "Package Registry" +slug: "overview" +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "Overview" + weight: 1 + identifier: "overview" +--- + +# Package Registry + +The Package Registry can be used as a public or private registry for common package managers. + +These package types are supported: + +- [Generic]({{< relref "doc/packages/generic.en-us.md" >}}) +- [NuGet]({{< relref "doc/packages/nuget.en-us.md" >}}) +- [npm]({{< relref "doc/packages/npm.en-us.md" >}}) +- [Maven]({{< relref "doc/packages/maven.en-us.md" >}}) +- [PyPI]({{< relref "doc/packages/pypi.en-us.md" >}}) +- [RubyGems]({{< relref "doc/packages/rubygems.en-us.md" >}}) + +**Table of Contents** + +{{< toc >}} + +## View packages + +You can view the packages of a repository on the repository page. + +1. Go to the repoistory. +1. Go to **Packages** in the navigation bar. + +To view more details about a package, select the name of the package. + +## Download a package + +To download a package from your repository: + +1. Go to **Packages** in the navigation bar. +1. Select the name of the package to view the details. +1. In the **Assets** section, select the name of the package file you want to download. + +## Delete a package + +You cannot edit a package after you published it in the Package Registry. Instead, you +must delete and recreate it. + +To delete a package from your repository: + +1. Go to **Packages** in the navigation bar. +1. Select the name of the package to view the details. +1. Click **Delete package** to permanently delete the package. + +## Disable the Package Registry + +The Package Registry is automatically enabled. To disable it for a single repository: + +1. Go to **Settings** in the navigation bar. +1. Disable **Enable Repository Packages Registry**. + +Previously published packages are not deleted by disabling the Package Registry. From fb4f03940efe364720a82fd2fdfdea8fb3b6eb53 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 7 Sep 2021 19:53:06 +0000 Subject: [PATCH 043/130] Added npm dependencies and keywords. --- integrations/api_packages_npm_test.go | 2 +- integrations/api_packages_nuget_test.go | 2 +- modules/packages/npm/creator.go | 54 ++++++++++--------- modules/packages/npm/metadata.go | 22 ++++---- options/locale/locale_en-US.ini | 7 +++ routers/api/v1/packages/npm/package.go | 2 +- routers/api/v1/packages/pypi/package.go | 2 +- templates/repo/packages/content/npm.tmpl | 24 +++++---- .../packages/content/npm_dependencies.tmpl | 18 +++++++ templates/repo/packages/content/rubygems.tmpl | 38 +------------ .../content/rubygems_dependencies.tmpl | 18 +++++++ 11 files changed, 106 insertions(+), 83 deletions(-) create mode 100644 templates/repo/packages/content/npm_dependencies.tmpl create mode 100644 templates/repo/packages/content/rubygems_dependencies.tmpl diff --git a/integrations/api_packages_npm_test.go b/integrations/api_packages_npm_test.go index b7ee8b7cd744a..83bab146157e0 100644 --- a/integrations/api_packages_npm_test.go +++ b/integrations/api_packages_npm_test.go @@ -19,7 +19,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestPackageNPM(t *testing.T) { +func TestPackageNpm(t *testing.T) { defer prepareTestEnv(t)() repository := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 2}).(*models.Repository) user := models.AssertExistsAndLoadBean(t, &models.User{ID: repository.OwnerID}).(*models.User) diff --git a/integrations/api_packages_nuget_test.go b/integrations/api_packages_nuget_test.go index b128264343d95..9be8315511912 100644 --- a/integrations/api_packages_nuget_test.go +++ b/integrations/api_packages_nuget_test.go @@ -127,7 +127,7 @@ func TestPackageNuGet(t *testing.T) { archive.Close() return &buf } - + req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage("unknown-package", "SymbolsPackage")) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusNotFound) diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index bcfaa769fa650..72f5fb842640f 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -37,7 +37,7 @@ var ( var nameMatch = regexp.MustCompile(`\A(@[^\/~'!\(\)\*]+?)[\/]([^_.][^\/~'!\(\)\*]+)\z`) -// Package represents a NPM package +// Package represents a npm package type Package struct { Name string Version string @@ -67,20 +67,22 @@ type PackageMetadata struct { // PackageMetadataVersion https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version type PackageMetadataVersion struct { - ID string `json:"_id"` - Name string `json:"name"` - Version string `json:"version"` - Description string `json:"description"` - Author User `json:"author"` - Homepage string `json:"homepage,omitempty"` - License string `json:"license,omitempty"` - Repository Repository `json:"repository,omitempty"` - Keywords []string `json:"keywords,omitempty"` - Dependencies map[string]string `json:"dependencies,omitempty"` - DevDependencies map[string]string `json:"devDependencies,omitempty"` - Readme string `json:"readme,omitempty"` - Dist PackageDistribution `json:"dist"` - Maintainers []User `json:"maintainers,omitempty"` + ID string `json:"_id"` + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Author User `json:"author"` + Homepage string `json:"homepage,omitempty"` + License string `json:"license,omitempty"` + Repository Repository `json:"repository,omitempty"` + Keywords []string `json:"keywords,omitempty"` + Dependencies map[string]string `json:"dependencies,omitempty"` + DevDependencies map[string]string `json:"devDependencies,omitempty"` + PeerDependencies map[string]string `json:"peerDependencies,omitempty"` + OptionalDependencies map[string]string `json:"optionalDependencies,omitempty"` + Readme string `json:"readme,omitempty"` + Dist PackageDistribution `json:"dist"` + Maintainers []User `json:"maintainers,omitempty"` } // PackageDistribution https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version @@ -144,7 +146,7 @@ type packageUpload struct { Attachments map[string]*PackageAttachment `json:"_attachments"` } -// ParsePackage parses the content into a NPM package +// ParsePackage parses the content into a npm package func ParsePackage(r io.Reader) (*Package, error) { var upload packageUpload if err := json.NewDecoder(r).Decode(&upload); err != nil { @@ -171,14 +173,18 @@ func ParsePackage(r io.Reader) (*Package, error) { Name: meta.Name, Version: meta.Version, Metadata: Metadata{ - Scope: nameParts[0], - Name: nameParts[1], - Description: meta.Description, - Author: meta.Author.Name, - License: meta.License, - ProjectURL: meta.Homepage, - Dependencies: meta.Dependencies, - Readme: meta.Readme, + Scope: nameParts[0], + Name: nameParts[1], + Description: meta.Description, + Author: meta.Author.Name, + License: meta.License, + ProjectURL: meta.Homepage, + Keywords: meta.Keywords, + Dependencies: meta.Dependencies, + DevelopmentDependencies: meta.DevDependencies, + PeerDependencies: meta.PeerDependencies, + OptionalDependencies: meta.OptionalDependencies, + Readme: meta.Readme, }, } diff --git a/modules/packages/npm/metadata.go b/modules/packages/npm/metadata.go index f0c9586ccc516..cb6589ae7895a 100644 --- a/modules/packages/npm/metadata.go +++ b/modules/packages/npm/metadata.go @@ -4,14 +4,18 @@ package npm -// Metadata represents the metadata of a NPM package +// Metadata represents the metadata of a npm package type Metadata struct { - Scope string `json:"scope"` - Name string `json:"name"` - Description string `json:"description"` - Author string `json:"author"` - License string `json:"license"` - ProjectURL string `json:"project_url"` - Dependencies map[string]string `json:"dependencies"` - Readme string `json:"readme"` + Scope string `json:"scope"` + Name string `json:"name"` + Description string `json:"description"` + Author string `json:"author"` + License string `json:"license"` + ProjectURL string `json:"project_url"` + Keywords []string `json:"keywords"` + Dependencies map[string]string `json:"dependencies"` + DevelopmentDependencies map[string]string `json:"development_dependencies"` + PeerDependencies map[string]string `json:"peer_dependencies"` + OptionalDependencies map[string]string `json:"optional_dependencies"` + Readme string `json:"readme"` } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 3b52097261799..68571dac3a8cd 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1521,6 +1521,7 @@ packages.installation = Installation packages.about = About this package packages.requirements = Requirements packages.dependencies = Dependencies +packages.keywords = Keywords packages.details = Details packages.details.author = Author packages.details.project_site = Project Site @@ -1543,11 +1544,17 @@ packages.nuget.documentation = For more information on the NuGet registry, see < packages.npm.registry = Setup this registry in your projects .npmrc file: packages.npm.use = To install the package use the following command: packages.npm.documentation = For more information on the npm registry, see the documentation. +packages.npm.dependencies = Dependencies +packages.npm.dependencies.development = Development Dependencies +packages.npm.dependencies.peer = Peer Dependencies +packages.npm.dependencies.optional = Optional Dependencies packages.pypi.requires = Requires Python packages.pypi.use = To install the package use the following command: packages.pypi.documentation = For more information on the PyPI registry, see the documentation. packages.rubygems.use_1 = Install from the command line packages.rubygems.use_2 = Install via Gemfile +packages.npm.dependencies.runtime = Runtime Dependencies +packages.npm.dependencies.development = Development Dependencies packages.rubygems.required.ruby = Requires Ruby version packages.rubygems.required.rubygems = Requires RubyGem version packages.rubygems.documentation = For more information on the RubyGems registry, see the documentation. diff --git a/routers/api/v1/packages/npm/package.go b/routers/api/v1/packages/npm/package.go index ad46d04576ba2..90a8b3c251288 100644 --- a/routers/api/v1/packages/npm/package.go +++ b/routers/api/v1/packages/npm/package.go @@ -14,7 +14,7 @@ import ( "github.com/hashicorp/go-version" ) -// Package represents a package with NPM metadata +// Package represents a package with npm metadata type Package struct { *models.Package *models.PackageFile diff --git a/routers/api/v1/packages/pypi/package.go b/routers/api/v1/packages/pypi/package.go index ae9692314608d..812bbadc5e04b 100644 --- a/routers/api/v1/packages/pypi/package.go +++ b/routers/api/v1/packages/pypi/package.go @@ -10,7 +10,7 @@ import ( pypi_module "code.gitea.io/gitea/modules/packages/pypi" ) -// Package represents a package with NPM metadata +// Package represents a package with PyPI metadata type Package struct { *models.Package Files []*models.PackageFile diff --git a/templates/repo/packages/content/npm.tmpl b/templates/repo/packages/content/npm.tmpl index 47557feeb7be5..6492d88a48a61 100644 --- a/templates/repo/packages/content/npm.tmpl +++ b/templates/repo/packages/content/npm.tmpl @@ -27,20 +27,24 @@
    {{end}} - {{if .Metadata.Dependencies}} + {{if or .Metadata.Dependencies .Metadata.DevelopmentDependencies .Metadata.PeerDependencies .Metadata.OptionalDependencies}}

    {{.i18n.Tr "repo.packages.dependencies"}}

    - {{range $dependency, $version := .Metadata.Dependencies}} -
    - {{svg "octicon-package-dependencies" 16 ""}} -
    -
    {{$dependency}}
    -
    {{$version}}
    -
    -
    - {{end}} + {{template "repo/packages/content/npm_dependencies" dict "dependencies" .Metadata.Dependencies "title" (.i18n.Tr "repo.packages.npm.dependencies")}} + {{template "repo/packages/content/npm_dependencies" dict "dependencies" .Metadata.DevelopmentDependencies "title" (.i18n.Tr "repo.packages.npm.dependencies.development")}} + {{template "repo/packages/content/npm_dependencies" dict "dependencies" .Metadata.PeerDependencies "title" (.i18n.Tr "repo.packages.npm.dependencies.peer")}} + {{template "repo/packages/content/npm_dependencies" dict "dependencies" .Metadata.OptionalDependencies "title" (.i18n.Tr "repo.packages.npm.dependencies.optional")}}
    {{end}} + + {{if or .Metadata.Keywords}} +

    {{.i18n.Tr "repo.packages.keywords"}}

    +
    + {{range .Metadata.Keywords}} + {{.}} + {{end}} +
    + {{end}} {{end}} diff --git a/templates/repo/packages/content/npm_dependencies.tmpl b/templates/repo/packages/content/npm_dependencies.tmpl new file mode 100644 index 0000000000000..764d1b68504e3 --- /dev/null +++ b/templates/repo/packages/content/npm_dependencies.tmpl @@ -0,0 +1,18 @@ +{{if .dependencies}} +
    +
    +
    {{.title}}
    +
    + {{range $dependency, $version := .dependencies}} +
    + {{svg "octicon-package-dependencies" 16 ""}} +
    +
    {{$dependency}}
    +
    {{$version}}
    +
    +
    + {{end}} +
    +
    +
    +{{end}} diff --git a/templates/repo/packages/content/rubygems.tmpl b/templates/repo/packages/content/rubygems.tmpl index 85a0d307aa737..2648c93e1f726 100644 --- a/templates/repo/packages/content/rubygems.tmpl +++ b/templates/repo/packages/content/rubygems.tmpl @@ -34,42 +34,8 @@ end

    {{.i18n.Tr "repo.packages.dependencies"}}

    - {{if .Metadata.RuntimeDependencies}} -
    -
    -
    Runtime Dependencies
    -
    - {{range .Metadata.RuntimeDependencies}} -
    - {{svg "octicon-package-dependencies" 16 ""}} -
    -
    {{.Name}}
    -
    {{range $i, $v := .Version}}{{if gt $i 0}}, {{end}}{{$v.Restriction}}{{$v.Version}}{{end}}
    -
    -
    - {{end}} -
    -
    -
    - {{end}} - {{if .Metadata.DevelopmentDependencies}} -
    -
    -
    Development Dependencies
    -
    - {{range .Metadata.DevelopmentDependencies}} -
    - {{svg "octicon-package-dependencies" 16 ""}} -
    -
    {{.Name}}
    -
    {{range $i, $v := .Version}}{{if gt $i 0}}, {{end}}{{$v.Restriction}}{{$v.Version}}{{end}}
    -
    -
    - {{end}} -
    -
    -
    - {{end}} + {{template "repo/packages/content/rubygems_dependencies" dict "dependencies" .Metadata.RuntimeDependencies "title" (.i18n.Tr "repo.packages.rubygems.dependencies.runtime")}} + {{template "repo/packages/content/rubygems_dependencies" dict "dependencies" .Metadata.DevelopmentDependencies "title" (.i18n.Tr "repo.packages.rubygems.dependencies.development")}}
    {{end}} diff --git a/templates/repo/packages/content/rubygems_dependencies.tmpl b/templates/repo/packages/content/rubygems_dependencies.tmpl new file mode 100644 index 0000000000000..48f9e15ab8c02 --- /dev/null +++ b/templates/repo/packages/content/rubygems_dependencies.tmpl @@ -0,0 +1,18 @@ +{{if .dependencies}} +
    +
    +
    {{.title}}
    +
    + {{range .dependencies}} +
    + {{svg "octicon-package-dependencies" 16 ""}} +
    +
    {{.Name}}
    +
    {{range $i, $v := .Version}}{{if gt $i 0}}, {{end}}{{$v.Restriction}}{{$v.Version}}{{end}}
    +
    +
    + {{end}} +
    +
    +
    +{{end}} From 08e6f82cc10f8f1c72437a4a76441cdd0f5a49d7 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 7 Sep 2021 19:56:37 +0000 Subject: [PATCH 044/130] Added no-packages information. --- models/package.go | 5 +++++ options/locale/locale_en-US.ini | 3 +++ routers/web/repo/packages.go | 7 +++++++ templates/repo/packages/list.tmpl | 12 +++++++++++- web_src/less/_repository.less | 11 +++++++++++ 5 files changed, 37 insertions(+), 1 deletion(-) diff --git a/models/package.go b/models/package.go index 82cba7dce5590..d48632e5cbcb8 100644 --- a/models/package.go +++ b/models/package.go @@ -256,6 +256,11 @@ func GetPackageByID(packageID int64) (*Package, error) { return p, nil } +// HasRepositoryPackages tests if a repository has packages +func HasRepositoryPackages(repositoryID int64) (bool, error) { + return x.Where("repo_id = ?", repositoryID).Exist(&Package{}) +} + // GetPackagesByRepository returns all packages of a repository func GetPackagesByRepository(repositoryID int64) ([]*Package, error) { packages := make([]*Package, 0, 10) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 68571dac3a8cd..107a0ad858158 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1514,8 +1514,11 @@ milestones.filter_sort.least_issues = Least issues packages = Packages packages.desc = Manage repository packages. +packages.empty = There are no packages yet. +packages.empty.documentation = For more information on the package registry, see the documentation. packages.filter.package_type = Type packages.filter.package_type.all = All +packages.filter.no_result = Your filter produced no results. packages.published_by = Published %[1]s by %[3]s packages.installation = Installation packages.about = About this package diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index f66fff9c25332..41f911b717a42 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -69,6 +69,13 @@ func Packages(ctx *context.Context) { } } + hasPackages, err := models.HasRepositoryPackages(repo.ID) + if err != nil { + ctx.ServerError("HasRepositoryPackages", err) + return + } + + ctx.Data["HasPackages"] = hasPackages ctx.Data["Packages"] = packages ctx.Data["Query"] = query ctx.Data["PackageType"] = packageType diff --git a/templates/repo/packages/list.tmpl b/templates/repo/packages/list.tmpl index 8ea24fd73fa98..209ed391dce10 100644 --- a/templates/repo/packages/list.tmpl +++ b/templates/repo/packages/list.tmpl @@ -33,7 +33,7 @@ -
    +
    {{range .Packages}}
  • @@ -48,6 +48,16 @@
  • + {{else}} + {{if not .HasPackages}} +
    + {{svg "octicon-package" 32}} +

    {{.i18n.Tr "repo.packages.empty"}}

    +

    {{.i18n.Tr "repo.packages.empty.documentation" | Safe}}

    +
    + {{else}} + {{.i18n.Tr "repo.packages.filter.no_result"}} + {{end}} {{end}} {{template "base/paginate" .}}
    diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index a58fe3698d2ec..89dfc3c89a45b 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -1977,6 +1977,17 @@ } } + &.packages { + .empty { + padding-top: 70px; + padding-bottom: 100px; + + .svg { + height: 48px; + } + } + } + &.wiki { &.start { .ui.segment { From fe02ca2486e2dff8a92dc2939fd56eb5abd05953 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 7 Sep 2021 20:05:04 +0000 Subject: [PATCH 045/130] Display file size. --- templates/repo/packages/view.tmpl | 3 ++- web_src/less/_repository.less | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/templates/repo/packages/view.tmpl b/templates/repo/packages/view.tmpl index 239c183f62918..e9a27b2c99538 100644 --- a/templates/repo/packages/view.tmpl +++ b/templates/repo/packages/view.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
    +
    {{template "repo/header" .}}
    @@ -40,6 +40,7 @@ {{range .Files}}
    {{.Name}} + {{FileSize .Size}}
    {{end}}
    diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index 89dfc3c89a45b..e6245e576d6dc 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -1986,6 +1986,10 @@ height: 48px; } } + + .file-size { + white-space: nowrap; + } } &.wiki { From 1cf894b0258de7e4ac3ad8b443b3d82885aa7ae6 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 7 Sep 2021 20:08:24 +0000 Subject: [PATCH 046/130] Display asset count. --- options/locale/locale_en-US.ini | 4 ++-- templates/repo/packages/view.tmpl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 107a0ad858158..e150555b2320c 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1556,8 +1556,8 @@ packages.pypi.use = To install the package use the following command: packages.pypi.documentation = For more information on the PyPI registry, see the documentation. packages.rubygems.use_1 = Install from the command line packages.rubygems.use_2 = Install via Gemfile -packages.npm.dependencies.runtime = Runtime Dependencies -packages.npm.dependencies.development = Development Dependencies +packages.rubygems.dependencies.runtime = Runtime Dependencies +packages.rubygems.dependencies.development = Development Dependencies packages.rubygems.required.ruby = Requires Ruby version packages.rubygems.required.rubygems = Requires RubyGem version packages.rubygems.documentation = For more information on the RubyGems registry, see the documentation. diff --git a/templates/repo/packages/view.tmpl b/templates/repo/packages/view.tmpl index e9a27b2c99538..f8d37a5c709d4 100644 --- a/templates/repo/packages/view.tmpl +++ b/templates/repo/packages/view.tmpl @@ -35,7 +35,7 @@ {{template "repo/packages/metadata/rubygems" .}}
    - {{.i18n.Tr "repo.packages.assets"}} + {{.i18n.Tr "repo.packages.assets"}} ({{len .Files}})
    {{range .Files}}
    From faf5518b9564826bd86e5ea26bc8fd4ffdb63a16 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 7 Sep 2021 20:34:22 +0000 Subject: [PATCH 047/130] Fixed filter alignment. --- templates/repo/packages/list.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/repo/packages/list.tmpl b/templates/repo/packages/list.tmpl index 209ed391dce10..6c74048b2c78d 100644 --- a/templates/repo/packages/list.tmpl +++ b/templates/repo/packages/list.tmpl @@ -13,8 +13,8 @@
    -
    -