Merge branch 'master' into feature-activitypub
This commit is contained in:
403
routers/api/packages/api.go
Normal file
403
routers/api/packages/api.go
Normal file
@@ -0,0 +1,403 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package packages
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/packages/composer"
|
||||
"code.gitea.io/gitea/routers/api/packages/conan"
|
||||
"code.gitea.io/gitea/routers/api/packages/container"
|
||||
"code.gitea.io/gitea/routers/api/packages/generic"
|
||||
"code.gitea.io/gitea/routers/api/packages/helm"
|
||||
"code.gitea.io/gitea/routers/api/packages/maven"
|
||||
"code.gitea.io/gitea/routers/api/packages/npm"
|
||||
"code.gitea.io/gitea/routers/api/packages/nuget"
|
||||
"code.gitea.io/gitea/routers/api/packages/pypi"
|
||||
"code.gitea.io/gitea/routers/api/packages/rubygems"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
context_service "code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
|
||||
return func(ctx *context.Context) {
|
||||
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`)
|
||||
ctx.Error(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Routes() *web.Route {
|
||||
r := web.NewRoute()
|
||||
|
||||
r.Use(context.PackageContexter())
|
||||
|
||||
authMethods := []auth.Method{
|
||||
&auth.OAuth2{},
|
||||
&auth.Basic{},
|
||||
&conan.Auth{},
|
||||
}
|
||||
if setting.Service.EnableReverseProxyAuth {
|
||||
authMethods = append(authMethods, &auth.ReverseProxy{})
|
||||
}
|
||||
|
||||
authGroup := auth.NewGroup(authMethods...)
|
||||
r.Use(func(ctx *context.Context) {
|
||||
ctx.Doer = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
|
||||
})
|
||||
|
||||
r.Group("/{username}", func() {
|
||||
r.Group("/composer", func() {
|
||||
r.Get("/packages.json", composer.ServiceIndex)
|
||||
r.Get("/search.json", composer.SearchPackages)
|
||||
r.Get("/list.json", composer.EnumeratePackages)
|
||||
r.Get("/p2/{vendorname}/{projectname}~dev.json", composer.PackageMetadata)
|
||||
r.Get("/p2/{vendorname}/{projectname}.json", composer.PackageMetadata)
|
||||
r.Get("/files/{package}/{version}/{filename}", composer.DownloadPackageFile)
|
||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), composer.UploadPackage)
|
||||
})
|
||||
r.Group("/conan", func() {
|
||||
r.Group("/v1", func() {
|
||||
r.Get("/ping", conan.Ping)
|
||||
r.Group("/users", func() {
|
||||
r.Get("/authenticate", conan.Authenticate)
|
||||
r.Get("/check_credentials", conan.CheckCredentials)
|
||||
})
|
||||
r.Group("/conans", func() {
|
||||
r.Get("/search", conan.SearchRecipes)
|
||||
r.Group("/{name}/{version}/{user}/{channel}", func() {
|
||||
r.Get("", conan.RecipeSnapshot)
|
||||
r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeleteRecipeV1)
|
||||
r.Get("/search", conan.SearchPackagesV1)
|
||||
r.Get("/digest", conan.RecipeDownloadURLs)
|
||||
r.Post("/upload_urls", reqPackageAccess(perm.AccessModeWrite), conan.RecipeUploadURLs)
|
||||
r.Get("/download_urls", conan.RecipeDownloadURLs)
|
||||
r.Group("/packages", func() {
|
||||
r.Post("/delete", reqPackageAccess(perm.AccessModeWrite), conan.DeletePackageV1)
|
||||
r.Group("/{package_reference}", func() {
|
||||
r.Get("", conan.PackageSnapshot)
|
||||
r.Get("/digest", conan.PackageDownloadURLs)
|
||||
r.Post("/upload_urls", reqPackageAccess(perm.AccessModeWrite), conan.PackageUploadURLs)
|
||||
r.Get("/download_urls", conan.PackageDownloadURLs)
|
||||
})
|
||||
})
|
||||
}, conan.ExtractPathParameters)
|
||||
})
|
||||
r.Group("/files/{name}/{version}/{user}/{channel}/{recipe_revision}", func() {
|
||||
r.Group("/recipe/{filename}", func() {
|
||||
r.Get("", conan.DownloadRecipeFile)
|
||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadRecipeFile)
|
||||
})
|
||||
r.Group("/package/{package_reference}/{package_revision}/{filename}", func() {
|
||||
r.Get("", conan.DownloadPackageFile)
|
||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadPackageFile)
|
||||
})
|
||||
}, conan.ExtractPathParameters)
|
||||
})
|
||||
r.Group("/v2", func() {
|
||||
r.Get("/ping", conan.Ping)
|
||||
r.Group("/users", func() {
|
||||
r.Get("/authenticate", conan.Authenticate)
|
||||
r.Get("/check_credentials", conan.CheckCredentials)
|
||||
})
|
||||
r.Group("/conans", func() {
|
||||
r.Get("/search", conan.SearchRecipes)
|
||||
r.Group("/{name}/{version}/{user}/{channel}", func() {
|
||||
r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeleteRecipeV2)
|
||||
r.Get("/search", conan.SearchPackagesV2)
|
||||
r.Get("/latest", conan.LatestRecipeRevision)
|
||||
r.Group("/revisions", func() {
|
||||
r.Get("", conan.ListRecipeRevisions)
|
||||
r.Group("/{recipe_revision}", func() {
|
||||
r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeleteRecipeV2)
|
||||
r.Get("/search", conan.SearchPackagesV2)
|
||||
r.Group("/files", func() {
|
||||
r.Get("", conan.ListRecipeRevisionFiles)
|
||||
r.Group("/{filename}", func() {
|
||||
r.Get("", conan.DownloadRecipeFile)
|
||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadRecipeFile)
|
||||
})
|
||||
})
|
||||
r.Group("/packages", func() {
|
||||
r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeletePackageV2)
|
||||
r.Group("/{package_reference}", func() {
|
||||
r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeletePackageV2)
|
||||
r.Get("/latest", conan.LatestPackageRevision)
|
||||
r.Group("/revisions", func() {
|
||||
r.Get("", conan.ListPackageRevisions)
|
||||
r.Group("/{package_revision}", func() {
|
||||
r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeletePackageV2)
|
||||
r.Group("/files", func() {
|
||||
r.Get("", conan.ListPackageRevisionFiles)
|
||||
r.Group("/{filename}", func() {
|
||||
r.Get("", conan.DownloadPackageFile)
|
||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadPackageFile)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}, conan.ExtractPathParameters)
|
||||
})
|
||||
})
|
||||
})
|
||||
r.Group("/generic", func() {
|
||||
r.Group("/{packagename}/{packageversion}/{filename}", func() {
|
||||
r.Get("", generic.DownloadPackageFile)
|
||||
r.Group("", func() {
|
||||
r.Put("", generic.UploadPackage)
|
||||
r.Delete("", generic.DeletePackage)
|
||||
}, reqPackageAccess(perm.AccessModeWrite))
|
||||
})
|
||||
})
|
||||
r.Group("/helm", func() {
|
||||
r.Get("/index.yaml", helm.Index)
|
||||
r.Get("/{filename}", helm.DownloadPackageFile)
|
||||
r.Post("/api/charts", reqPackageAccess(perm.AccessModeWrite), helm.UploadPackage)
|
||||
})
|
||||
r.Group("/maven", func() {
|
||||
r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile)
|
||||
r.Get("/*", maven.DownloadPackageFile)
|
||||
})
|
||||
r.Group("/nuget", func() {
|
||||
r.Get("/index.json", nuget.ServiceIndex)
|
||||
r.Get("/query", nuget.SearchService)
|
||||
r.Group("/registration/{id}", func() {
|
||||
r.Get("/index.json", nuget.RegistrationIndex)
|
||||
r.Get("/{version}", nuget.RegistrationLeaf)
|
||||
})
|
||||
r.Group("/package/{id}", func() {
|
||||
r.Get("/index.json", nuget.EnumeratePackageVersions)
|
||||
r.Get("/{version}/{filename}", nuget.DownloadPackageFile)
|
||||
})
|
||||
r.Group("", func() {
|
||||
r.Put("/", nuget.UploadPackage)
|
||||
r.Put("/symbolpackage", nuget.UploadSymbolPackage)
|
||||
r.Delete("/{id}/{version}", nuget.DeletePackage)
|
||||
}, reqPackageAccess(perm.AccessModeWrite))
|
||||
r.Get("/symbols/{filename}/{guid:[0-9a-f]{32}}FFFFFFFF/{filename2}", nuget.DownloadSymbolFile)
|
||||
})
|
||||
r.Group("/npm", func() {
|
||||
r.Group("/@{scope}/{id}", func() {
|
||||
r.Get("", npm.PackageMetadata)
|
||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
|
||||
r.Get("/-/{version}/{filename}", npm.DownloadPackageFile)
|
||||
})
|
||||
r.Group("/{id}", func() {
|
||||
r.Get("", npm.PackageMetadata)
|
||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
|
||||
r.Get("/-/{version}/{filename}", npm.DownloadPackageFile)
|
||||
})
|
||||
r.Group("/-/package/@{scope}/{id}/dist-tags", func() {
|
||||
r.Get("", npm.ListPackageTags)
|
||||
r.Group("/{tag}", func() {
|
||||
r.Put("", npm.AddPackageTag)
|
||||
r.Delete("", npm.DeletePackageTag)
|
||||
}, reqPackageAccess(perm.AccessModeWrite))
|
||||
})
|
||||
r.Group("/-/package/{id}/dist-tags", func() {
|
||||
r.Get("", npm.ListPackageTags)
|
||||
r.Group("/{tag}", func() {
|
||||
r.Put("", npm.AddPackageTag)
|
||||
r.Delete("", npm.DeletePackageTag)
|
||||
}, reqPackageAccess(perm.AccessModeWrite))
|
||||
})
|
||||
})
|
||||
r.Group("/pypi", func() {
|
||||
r.Post("/", reqPackageAccess(perm.AccessModeWrite), pypi.UploadPackageFile)
|
||||
r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile)
|
||||
r.Get("/simple/{id}", pypi.PackageMetadata)
|
||||
})
|
||||
r.Group("/rubygems", func() {
|
||||
r.Get("/specs.4.8.gz", rubygems.EnumeratePackages)
|
||||
r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest)
|
||||
r.Get("/prerelease_specs.4.8.gz", rubygems.EnumeratePackagesPreRelease)
|
||||
r.Get("/quick/Marshal.4.8/{filename}", rubygems.ServePackageSpecification)
|
||||
r.Get("/gems/{filename}", rubygems.DownloadPackageFile)
|
||||
r.Group("/api/v1/gems", func() {
|
||||
r.Post("/", rubygems.UploadPackageFile)
|
||||
r.Delete("/yank", rubygems.DeletePackage)
|
||||
}, reqPackageAccess(perm.AccessModeWrite))
|
||||
})
|
||||
}, context_service.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func ContainerRoutes() *web.Route {
|
||||
r := web.NewRoute()
|
||||
|
||||
r.Use(context.PackageContexter())
|
||||
|
||||
authMethods := []auth.Method{
|
||||
&auth.Basic{},
|
||||
&container.Auth{},
|
||||
}
|
||||
if setting.Service.EnableReverseProxyAuth {
|
||||
authMethods = append(authMethods, &auth.ReverseProxy{})
|
||||
}
|
||||
|
||||
authGroup := auth.NewGroup(authMethods...)
|
||||
r.Use(func(ctx *context.Context) {
|
||||
ctx.Doer = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
|
||||
})
|
||||
|
||||
r.Get("", container.ReqContainerAccess, container.DetermineSupport)
|
||||
r.Get("/token", container.Authenticate)
|
||||
r.Group("/{username}", func() {
|
||||
r.Group("/{image}", func() {
|
||||
r.Group("/blobs/uploads", func() {
|
||||
r.Post("", container.InitiateUploadBlob)
|
||||
r.Group("/{uuid}", func() {
|
||||
r.Patch("", container.UploadBlob)
|
||||
r.Put("", container.EndUploadBlob)
|
||||
})
|
||||
}, reqPackageAccess(perm.AccessModeWrite))
|
||||
r.Group("/blobs/{digest}", func() {
|
||||
r.Head("", container.HeadBlob)
|
||||
r.Get("", container.GetBlob)
|
||||
r.Delete("", reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob)
|
||||
})
|
||||
r.Group("/manifests/{reference}", func() {
|
||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), container.UploadManifest)
|
||||
r.Head("", container.HeadManifest)
|
||||
r.Get("", container.GetManifest)
|
||||
r.Delete("", reqPackageAccess(perm.AccessModeWrite), container.DeleteManifest)
|
||||
})
|
||||
r.Get("/tags/list", container.GetTagList)
|
||||
}, container.VerifyImageName)
|
||||
|
||||
var (
|
||||
blobsUploadsPattern = regexp.MustCompile(`\A(.+)/blobs/uploads/([a-zA-Z0-9-_.=]+)\z`)
|
||||
blobsPattern = regexp.MustCompile(`\A(.+)/blobs/([^/]+)\z`)
|
||||
manifestsPattern = regexp.MustCompile(`\A(.+)/manifests/([^/]+)\z`)
|
||||
)
|
||||
|
||||
// Manual mapping of routes because {image} can contain slashes which chi does not support
|
||||
r.Route("/*", "HEAD,GET,POST,PUT,PATCH,DELETE", func(ctx *context.Context) {
|
||||
path := ctx.Params("*")
|
||||
isHead := ctx.Req.Method == "HEAD"
|
||||
isGet := ctx.Req.Method == "GET"
|
||||
isPost := ctx.Req.Method == "POST"
|
||||
isPut := ctx.Req.Method == "PUT"
|
||||
isPatch := ctx.Req.Method == "PATCH"
|
||||
isDelete := ctx.Req.Method == "DELETE"
|
||||
|
||||
if isPost && strings.HasSuffix(path, "/blobs/uploads") {
|
||||
reqPackageAccess(perm.AccessModeWrite)(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetParams("image", path[:len(path)-14])
|
||||
container.VerifyImageName(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
container.InitiateUploadBlob(ctx)
|
||||
return
|
||||
}
|
||||
if isGet && strings.HasSuffix(path, "/tags/list") {
|
||||
ctx.SetParams("image", path[:len(path)-10])
|
||||
container.VerifyImageName(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
container.GetTagList(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
m := blobsUploadsPattern.FindStringSubmatch(path)
|
||||
if len(m) == 3 && (isPut || isPatch) {
|
||||
reqPackageAccess(perm.AccessModeWrite)(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetParams("image", m[1])
|
||||
container.VerifyImageName(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetParams("uuid", m[2])
|
||||
|
||||
if isPatch {
|
||||
container.UploadBlob(ctx)
|
||||
} else {
|
||||
container.EndUploadBlob(ctx)
|
||||
}
|
||||
return
|
||||
}
|
||||
m = blobsPattern.FindStringSubmatch(path)
|
||||
if len(m) == 3 && (isHead || isGet || isDelete) {
|
||||
ctx.SetParams("image", m[1])
|
||||
container.VerifyImageName(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetParams("digest", m[2])
|
||||
|
||||
if isHead {
|
||||
container.HeadBlob(ctx)
|
||||
} else if isGet {
|
||||
container.GetBlob(ctx)
|
||||
} else {
|
||||
reqPackageAccess(perm.AccessModeWrite)(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
container.DeleteBlob(ctx)
|
||||
}
|
||||
return
|
||||
}
|
||||
m = manifestsPattern.FindStringSubmatch(path)
|
||||
if len(m) == 3 && (isHead || isGet || isPut || isDelete) {
|
||||
ctx.SetParams("image", m[1])
|
||||
container.VerifyImageName(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetParams("reference", m[2])
|
||||
|
||||
if isHead {
|
||||
container.HeadManifest(ctx)
|
||||
} else if isGet {
|
||||
container.GetManifest(ctx)
|
||||
} else {
|
||||
reqPackageAccess(perm.AccessModeWrite)(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if isPut {
|
||||
container.UploadManifest(ctx)
|
||||
} else {
|
||||
container.DeleteManifest(ctx)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNotFound)
|
||||
})
|
||||
}, container.ReqContainerAccess, context_service.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
|
||||
|
||||
return r
|
||||
}
|
||||
118
routers/api/packages/composer/api.go
Normal file
118
routers/api/packages/composer/api.go
Normal file
@@ -0,0 +1,118 @@
|
||||
// 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 composer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
composer_module "code.gitea.io/gitea/modules/packages/composer"
|
||||
)
|
||||
|
||||
// ServiceIndexResponse contains registry endpoints
|
||||
type ServiceIndexResponse struct {
|
||||
SearchTemplate string `json:"search"`
|
||||
MetadataTemplate string `json:"metadata-url"`
|
||||
PackageList string `json:"list"`
|
||||
}
|
||||
|
||||
func createServiceIndexResponse(registryURL string) *ServiceIndexResponse {
|
||||
return &ServiceIndexResponse{
|
||||
SearchTemplate: registryURL + "/search.json?q=%query%&type=%type%",
|
||||
MetadataTemplate: registryURL + "/p2/%package%.json",
|
||||
PackageList: registryURL + "/list.json",
|
||||
}
|
||||
}
|
||||
|
||||
// SearchResultResponse contains search results
|
||||
type SearchResultResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
Results []*SearchResult `json:"results"`
|
||||
NextLink string `json:"next,omitempty"`
|
||||
}
|
||||
|
||||
// SearchResult contains a search result
|
||||
type SearchResult struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Downloads int64 `json:"downloads"`
|
||||
}
|
||||
|
||||
func createSearchResultResponse(total int64, pds []*packages_model.PackageDescriptor, nextLink string) *SearchResultResponse {
|
||||
results := make([]*SearchResult, 0, len(pds))
|
||||
|
||||
for _, pd := range pds {
|
||||
results = append(results, &SearchResult{
|
||||
Name: pd.Package.Name,
|
||||
Description: pd.Metadata.(*composer_module.Metadata).Description,
|
||||
Downloads: pd.Version.DownloadCount,
|
||||
})
|
||||
}
|
||||
|
||||
return &SearchResultResponse{
|
||||
Total: total,
|
||||
Results: results,
|
||||
NextLink: nextLink,
|
||||
}
|
||||
}
|
||||
|
||||
// PackageMetadataResponse contains packages metadata
|
||||
type PackageMetadataResponse struct {
|
||||
Minified string `json:"minified"`
|
||||
Packages map[string][]*PackageVersionMetadata `json:"packages"`
|
||||
}
|
||||
|
||||
// PackageVersionMetadata contains package metadata
|
||||
type PackageVersionMetadata struct {
|
||||
*composer_module.Metadata
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"`
|
||||
Created time.Time `json:"time"`
|
||||
Dist Dist `json:"dist"`
|
||||
}
|
||||
|
||||
// Dist contains package download informations
|
||||
type Dist struct {
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
Checksum string `json:"shasum"`
|
||||
}
|
||||
|
||||
func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *PackageMetadataResponse {
|
||||
versions := make([]*PackageVersionMetadata, 0, len(pds))
|
||||
|
||||
for _, pd := range pds {
|
||||
packageType := ""
|
||||
for _, pvp := range pd.Properties {
|
||||
if pvp.Name == composer_module.TypeProperty {
|
||||
packageType = pvp.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
versions = append(versions, &PackageVersionMetadata{
|
||||
Name: pd.Package.Name,
|
||||
Version: pd.Version.Version,
|
||||
Type: packageType,
|
||||
Created: time.Unix(int64(pd.Version.CreatedUnix), 0),
|
||||
Metadata: pd.Metadata.(*composer_module.Metadata),
|
||||
Dist: Dist{
|
||||
Type: "zip",
|
||||
URL: fmt.Sprintf("%s/files/%s/%s/%s", registryURL, url.PathEscape(pd.Package.LowerName), url.PathEscape(pd.Version.LowerVersion), url.PathEscape(pd.Files[0].File.LowerName)),
|
||||
Checksum: pd.Files[0].Blob.HashSHA1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return &PackageMetadataResponse{
|
||||
Minified: "composer/2.0",
|
||||
Packages: map[string][]*PackageVersionMetadata{
|
||||
pds[0].Package.Name: versions,
|
||||
},
|
||||
}
|
||||
}
|
||||
250
routers/api/packages/composer/composer.go
Normal file
250
routers/api/packages/composer/composer.go
Normal file
@@ -0,0 +1,250 @@
|
||||
// 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 composer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
composer_module "code.gitea.io/gitea/modules/packages/composer"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
func apiError(ctx *context.Context, status int, obj interface{}) {
|
||||
helper.LogAndProcessError(ctx, status, obj, func(message string) {
|
||||
type Error struct {
|
||||
Status int `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
ctx.JSON(status, struct {
|
||||
Errors []Error `json:"errors"`
|
||||
}{
|
||||
Errors: []Error{
|
||||
{Status: status, Message: message},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ServiceIndex displays registry endpoints
|
||||
func ServiceIndex(ctx *context.Context) {
|
||||
resp := createServiceIndexResponse(setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/composer")
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// SearchPackages searches packages, only "q" is supported
|
||||
// https://packagist.org/apidoc#search-packages
|
||||
func SearchPackages(ctx *context.Context) {
|
||||
page := ctx.FormInt("page")
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
perPage := ctx.FormInt("per_page")
|
||||
paginator := db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: convert.ToCorrectPageSize(perPage),
|
||||
}
|
||||
|
||||
opts := &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeComposer,
|
||||
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
|
||||
Paginator: &paginator,
|
||||
}
|
||||
if ctx.FormTrim("type") != "" {
|
||||
opts.Properties = map[string]string{
|
||||
composer_module.TypeProperty: ctx.FormTrim("type"),
|
||||
}
|
||||
}
|
||||
|
||||
pvs, total, err := packages_model.SearchLatestVersions(ctx, opts)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
nextLink := ""
|
||||
if len(pvs) == paginator.PageSize {
|
||||
u, err := url.Parse(setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/composer/search.json")
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("q", ctx.FormTrim("q"))
|
||||
q.Set("type", ctx.FormTrim("type"))
|
||||
q.Set("page", strconv.Itoa(page+1))
|
||||
if perPage != 0 {
|
||||
q.Set("per_page", strconv.Itoa(perPage))
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
nextLink = u.String()
|
||||
}
|
||||
|
||||
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := createSearchResultResponse(total, pds, nextLink)
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// EnumeratePackages lists all package names
|
||||
// https://packagist.org/apidoc#list-packages
|
||||
func EnumeratePackages(ctx *context.Context) {
|
||||
ps, err := packages_model.GetPackagesByType(db.DefaultContext, ctx.Package.Owner.ID, packages_model.TypeComposer)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(ps))
|
||||
for _, p := range ps {
|
||||
names = append(names, p.Name)
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string][]string{
|
||||
"packageNames": names,
|
||||
})
|
||||
}
|
||||
|
||||
// PackageMetadata returns the metadata for a single package
|
||||
// https://packagist.org/apidoc#get-package-data
|
||||
func PackageMetadata(ctx *context.Context) {
|
||||
vendorName := ctx.Params("vendorname")
|
||||
projectName := ctx.Params("projectname")
|
||||
|
||||
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeComposer, vendorName+"/"+projectName)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(pvs) == 0 {
|
||||
apiError(ctx, http.StatusNotFound, packages_model.ErrPackageNotExist)
|
||||
return
|
||||
}
|
||||
|
||||
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := createPackageMetadataResponse(
|
||||
setting.AppURL+"api/packages/"+ctx.Package.Owner.Name+"/composer",
|
||||
pds,
|
||||
)
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// DownloadPackageFile serves the content of a package
|
||||
func DownloadPackageFile(ctx *context.Context) {
|
||||
s, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
|
||||
ctx,
|
||||
&packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeComposer,
|
||||
Name: ctx.Params("package"),
|
||||
Version: ctx.Params("version"),
|
||||
},
|
||||
&packages_service.PackageFileInfo{
|
||||
Filename: ctx.Params("filename"),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
ctx.ServeStream(s, pf.Name)
|
||||
}
|
||||
|
||||
// UploadPackage creates a new package
|
||||
func UploadPackage(ctx *context.Context) {
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body, 32*1024*1024)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer buf.Close()
|
||||
|
||||
cp, err := composer_module.ParsePackage(buf, buf.Size())
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if cp.Version == "" {
|
||||
v, err := version.NewVersion(ctx.FormTrim("version"))
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, composer_module.ErrInvalidVersion)
|
||||
return
|
||||
}
|
||||
cp.Version = v.String()
|
||||
}
|
||||
|
||||
_, _, err = packages_service.CreatePackageAndAddFile(
|
||||
&packages_service.PackageCreationInfo{
|
||||
PackageInfo: packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeComposer,
|
||||
Name: cp.Name,
|
||||
Version: cp.Version,
|
||||
},
|
||||
SemverCompatible: true,
|
||||
Creator: ctx.Doer,
|
||||
Metadata: cp.Metadata,
|
||||
Properties: map[string]string{
|
||||
composer_module.TypeProperty: cp.Type,
|
||||
},
|
||||
},
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: strings.ToLower(fmt.Sprintf("%s.%s.zip", strings.ReplaceAll(cp.Name, "/", "-"), cp.Version)),
|
||||
},
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageVersion {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
41
routers/api/packages/conan/auth.go
Normal file
41
routers/api/packages/conan/auth.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package conan
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
type Auth struct{}
|
||||
|
||||
func (a *Auth) Name() string {
|
||||
return "conan"
|
||||
}
|
||||
|
||||
// Verify extracts the user from the Bearer token
|
||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) *user_model.User {
|
||||
uid, err := packages.ParseAuthorizationToken(req)
|
||||
if err != nil {
|
||||
log.Trace("ParseAuthorizationToken: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if uid == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
u, err := user_model.GetUserByID(uid)
|
||||
if err != nil {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
||||
818
routers/api/packages/conan/conan.go
Normal file
818
routers/api/packages/conan/conan.go
Normal file
@@ -0,0 +1,818 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package conan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
conan_model "code.gitea.io/gitea/models/packages/conan"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"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"
|
||||
conan_module "code.gitea.io/gitea/modules/packages/conan"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
const (
|
||||
conanfileFile = "conanfile.py"
|
||||
conaninfoFile = "conaninfo.txt"
|
||||
|
||||
recipeReferenceKey = "RecipeReference"
|
||||
packageReferenceKey = "PackageReference"
|
||||
)
|
||||
|
||||
type stringSet map[string]struct{}
|
||||
|
||||
var (
|
||||
recipeFileList = stringSet{
|
||||
conanfileFile: struct{}{},
|
||||
"conanmanifest.txt": struct{}{},
|
||||
"conan_sources.tgz": struct{}{},
|
||||
"conan_export.tgz": struct{}{},
|
||||
}
|
||||
packageFileList = stringSet{
|
||||
conaninfoFile: struct{}{},
|
||||
"conanmanifest.txt": struct{}{},
|
||||
"conan_package.tgz": struct{}{},
|
||||
}
|
||||
)
|
||||
|
||||
func jsonResponse(ctx *context.Context, status int, obj interface{}) {
|
||||
// https://github.com/conan-io/conan/issues/6613
|
||||
ctx.Resp.Header().Set("Content-Type", "application/json")
|
||||
ctx.Status(status)
|
||||
if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil {
|
||||
log.Error("JSON encode: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func apiError(ctx *context.Context, status int, obj interface{}) {
|
||||
helper.LogAndProcessError(ctx, status, obj, func(message string) {
|
||||
jsonResponse(ctx, status, map[string]string{
|
||||
"message": message,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func baseURL(ctx *context.Context) string {
|
||||
return setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/conan"
|
||||
}
|
||||
|
||||
// ExtractPathParameters is a middleware to extract common parameters from path
|
||||
func ExtractPathParameters(ctx *context.Context) {
|
||||
rref, err := conan_module.NewRecipeReference(
|
||||
ctx.Params("name"),
|
||||
ctx.Params("version"),
|
||||
ctx.Params("user"),
|
||||
ctx.Params("channel"),
|
||||
ctx.Params("recipe_revision"),
|
||||
)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data[recipeReferenceKey] = rref
|
||||
|
||||
reference := ctx.Params("package_reference")
|
||||
|
||||
var pref *conan_module.PackageReference
|
||||
if reference != "" {
|
||||
pref, err = conan_module.NewPackageReference(
|
||||
rref,
|
||||
reference,
|
||||
ctx.Params("package_revision"),
|
||||
)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data[packageReferenceKey] = pref
|
||||
}
|
||||
|
||||
// Ping reports the server capabilities
|
||||
func Ping(ctx *context.Context) {
|
||||
ctx.RespHeader().Add("X-Conan-Server-Capabilities", "revisions") // complex_search,checksum_deploy,matrix_params
|
||||
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// Authenticate creates an authentication token for the user
|
||||
func Authenticate(ctx *context.Context) {
|
||||
if ctx.Doer == nil {
|
||||
apiError(ctx, http.StatusBadRequest, nil)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := packages_service.CreateAuthorizationToken(ctx.Doer)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.PlainText(http.StatusOK, token)
|
||||
}
|
||||
|
||||
// CheckCredentials tests if the provided authentication token is valid
|
||||
func CheckCredentials(ctx *context.Context) {
|
||||
if ctx.Doer == nil {
|
||||
ctx.Status(http.StatusUnauthorized)
|
||||
} else {
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// RecipeSnapshot displays the recipe files with their md5 hash
|
||||
func RecipeSnapshot(ctx *context.Context) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
|
||||
serveSnapshot(ctx, rref.AsKey())
|
||||
}
|
||||
|
||||
// RecipeSnapshot displays the package files with their md5 hash
|
||||
func PackageSnapshot(ctx *context.Context) {
|
||||
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
||||
|
||||
serveSnapshot(ctx, pref.AsKey())
|
||||
}
|
||||
|
||||
func serveSnapshot(ctx *context.Context, fileKey string) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
||||
VersionID: pv.ID,
|
||||
CompositeKey: fileKey,
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(pfs) == 0 {
|
||||
apiError(ctx, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
files := make(map[string]string)
|
||||
for _, pf := range pfs {
|
||||
pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
files[pf.Name] = pb.HashMD5
|
||||
}
|
||||
|
||||
jsonResponse(ctx, http.StatusOK, files)
|
||||
}
|
||||
|
||||
// RecipeDownloadURLs displays the recipe files with their download url
|
||||
func RecipeDownloadURLs(ctx *context.Context) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
|
||||
serveDownloadURLs(
|
||||
ctx,
|
||||
rref.AsKey(),
|
||||
fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/recipe", rref.LinkName()),
|
||||
)
|
||||
}
|
||||
|
||||
// PackageDownloadURLs displays the package files with their download url
|
||||
func PackageDownloadURLs(ctx *context.Context) {
|
||||
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
||||
|
||||
serveDownloadURLs(
|
||||
ctx,
|
||||
pref.AsKey(),
|
||||
fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/package/%s", pref.Recipe.LinkName(), pref.LinkName()),
|
||||
)
|
||||
}
|
||||
|
||||
func serveDownloadURLs(ctx *context.Context, fileKey, downloadURL string) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
||||
VersionID: pv.ID,
|
||||
CompositeKey: fileKey,
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(pfs) == 0 {
|
||||
apiError(ctx, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
urls := make(map[string]string)
|
||||
for _, pf := range pfs {
|
||||
urls[pf.Name] = fmt.Sprintf("%s/%s", downloadURL, pf.Name)
|
||||
}
|
||||
|
||||
jsonResponse(ctx, http.StatusOK, urls)
|
||||
}
|
||||
|
||||
// RecipeUploadURLs displays the upload urls for the provided recipe files
|
||||
func RecipeUploadURLs(ctx *context.Context) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
|
||||
serveUploadURLs(
|
||||
ctx,
|
||||
recipeFileList,
|
||||
fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/recipe", rref.LinkName()),
|
||||
)
|
||||
}
|
||||
|
||||
// PackageUploadURLs displays the upload urls for the provided package files
|
||||
func PackageUploadURLs(ctx *context.Context) {
|
||||
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
||||
|
||||
serveUploadURLs(
|
||||
ctx,
|
||||
packageFileList,
|
||||
fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/package/%s", pref.Recipe.LinkName(), pref.LinkName()),
|
||||
)
|
||||
}
|
||||
|
||||
func serveUploadURLs(ctx *context.Context, fileFilter stringSet, uploadURL string) {
|
||||
defer ctx.Req.Body.Close()
|
||||
|
||||
var files map[string]int64
|
||||
if err := json.NewDecoder(ctx.Req.Body).Decode(&files); err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
urls := make(map[string]string)
|
||||
for file := range files {
|
||||
if _, ok := fileFilter[file]; ok {
|
||||
urls[file] = fmt.Sprintf("%s/%s", uploadURL, file)
|
||||
}
|
||||
}
|
||||
|
||||
jsonResponse(ctx, http.StatusOK, urls)
|
||||
}
|
||||
|
||||
// UploadRecipeFile handles the upload of a recipe file
|
||||
func UploadRecipeFile(ctx *context.Context) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
|
||||
uploadFile(ctx, recipeFileList, rref.AsKey())
|
||||
}
|
||||
|
||||
// UploadPackageFile handles the upload of a package file
|
||||
func UploadPackageFile(ctx *context.Context) {
|
||||
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
||||
|
||||
uploadFile(ctx, packageFileList, pref.AsKey())
|
||||
}
|
||||
|
||||
func uploadFile(ctx *context.Context, fileFilter stringSet, fileKey string) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
||||
|
||||
filename := ctx.Params("filename")
|
||||
if _, ok := fileFilter[filename]; !ok {
|
||||
apiError(ctx, http.StatusBadRequest, nil)
|
||||
return
|
||||
}
|
||||
|
||||
upload, close, err := ctx.UploadStream()
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
if close {
|
||||
defer upload.Close()
|
||||
}
|
||||
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(upload, 32*1024*1024)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer buf.Close()
|
||||
|
||||
if buf.Size() == 0 {
|
||||
// ignore empty uploads, second request contains content
|
||||
jsonResponse(ctx, http.StatusOK, nil)
|
||||
return
|
||||
}
|
||||
|
||||
isConanfileFile := filename == conanfileFile
|
||||
|
||||
pci := &packages_service.PackageCreationInfo{
|
||||
PackageInfo: packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeConan,
|
||||
Name: rref.Name,
|
||||
Version: rref.Version,
|
||||
},
|
||||
SemverCompatible: true,
|
||||
Creator: ctx.Doer,
|
||||
}
|
||||
pfci := &packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: strings.ToLower(filename),
|
||||
CompositeKey: fileKey,
|
||||
},
|
||||
Data: buf,
|
||||
IsLead: isConanfileFile,
|
||||
Properties: map[string]string{
|
||||
conan_module.PropertyRecipeUser: rref.User,
|
||||
conan_module.PropertyRecipeChannel: rref.Channel,
|
||||
conan_module.PropertyRecipeRevision: rref.RevisionOrDefault(),
|
||||
},
|
||||
OverwriteExisting: true,
|
||||
}
|
||||
|
||||
if pref != nil {
|
||||
pfci.Properties[conan_module.PropertyPackageReference] = pref.Reference
|
||||
pfci.Properties[conan_module.PropertyPackageRevision] = pref.RevisionOrDefault()
|
||||
}
|
||||
|
||||
if isConanfileFile || filename == conaninfoFile {
|
||||
if isConanfileFile {
|
||||
metadata, err := conan_module.ParseConanfile(buf)
|
||||
if err != nil {
|
||||
log.Error("Error parsing package metadata: %v", err)
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pci.Owner.ID, pci.PackageType, pci.Name, pci.Version)
|
||||
if err != nil && err != packages_model.ErrPackageNotExist {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if pv != nil {
|
||||
raw, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
pv.MetadataJSON = string(raw)
|
||||
if err := packages_model.UpdateVersion(ctx, pv); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
pci.Metadata = metadata
|
||||
}
|
||||
} else {
|
||||
info, err := conan_module.ParseConaninfo(buf)
|
||||
if err != nil {
|
||||
log.Error("Error parsing conan info: %v", err)
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
raw, err := json.Marshal(info)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
pfci.Properties[conan_module.PropertyPackageInfo] = string(raw)
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
|
||||
pci,
|
||||
pfci,
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageFile {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
// DownloadRecipeFile serves the conent of the requested recipe file
|
||||
func DownloadRecipeFile(ctx *context.Context) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
|
||||
downloadFile(ctx, recipeFileList, rref.AsKey())
|
||||
}
|
||||
|
||||
// DownloadPackageFile serves the conent of the requested package file
|
||||
func DownloadPackageFile(ctx *context.Context) {
|
||||
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
||||
|
||||
downloadFile(ctx, packageFileList, pref.AsKey())
|
||||
}
|
||||
|
||||
func downloadFile(ctx *context.Context, fileFilter stringSet, fileKey string) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
|
||||
filename := ctx.Params("filename")
|
||||
if _, ok := fileFilter[filename]; !ok {
|
||||
apiError(ctx, http.StatusBadRequest, nil)
|
||||
return
|
||||
}
|
||||
|
||||
s, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
|
||||
ctx,
|
||||
&packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeConan,
|
||||
Name: rref.Name,
|
||||
Version: rref.Version,
|
||||
},
|
||||
&packages_service.PackageFileInfo{
|
||||
Filename: filename,
|
||||
CompositeKey: fileKey,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
ctx.ServeStream(s, pf.Name)
|
||||
}
|
||||
|
||||
// DeleteRecipeV1 deletes the requested recipe(s)
|
||||
func DeleteRecipeV1(ctx *context.Context) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
|
||||
if err := deleteRecipeOrPackage(ctx, rref, true, nil, false); err != nil {
|
||||
if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// DeleteRecipeV2 deletes the requested recipe(s) respecting its revisions
|
||||
func DeleteRecipeV2(ctx *context.Context) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
|
||||
if err := deleteRecipeOrPackage(ctx, rref, rref.Revision == "", nil, false); err != nil {
|
||||
if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// DeletePackageV1 deletes the requested package(s)
|
||||
func DeletePackageV1(ctx *context.Context) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
|
||||
type PackageReferences struct {
|
||||
References []string `json:"package_ids"`
|
||||
}
|
||||
|
||||
var ids *PackageReferences
|
||||
if err := json.NewDecoder(ctx.Req.Body).Decode(&ids); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
revisions, err := conan_model.GetRecipeRevisions(ctx, ctx.Package.Owner.ID, rref)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
for _, revision := range revisions {
|
||||
currentRref := rref.WithRevision(revision.Value)
|
||||
|
||||
var references []*conan_model.PropertyValue
|
||||
if len(ids.References) == 0 {
|
||||
if references, err = conan_model.GetPackageReferences(ctx, ctx.Package.Owner.ID, currentRref); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
for _, reference := range ids.References {
|
||||
references = append(references, &conan_model.PropertyValue{Value: reference})
|
||||
}
|
||||
}
|
||||
|
||||
for _, reference := range references {
|
||||
pref, _ := conan_module.NewPackageReference(currentRref, reference.Value, conan_module.DefaultRevision)
|
||||
if err := deleteRecipeOrPackage(ctx, currentRref, true, pref, true); err != nil {
|
||||
if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// DeletePackageV2 deletes the requested package(s) respecting its revisions
|
||||
func DeletePackageV2(ctx *context.Context) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
||||
|
||||
if pref != nil { // has package reference
|
||||
if err := deleteRecipeOrPackage(ctx, rref, false, pref, pref.Revision == ""); err != nil {
|
||||
if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
} else {
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
references, err := conan_model.GetPackageReferences(ctx, ctx.Package.Owner.ID, rref)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(references) == 0 {
|
||||
apiError(ctx, http.StatusNotFound, conan_model.ErrPackageReferenceNotExist)
|
||||
return
|
||||
}
|
||||
|
||||
for _, reference := range references {
|
||||
pref, _ := conan_module.NewPackageReference(rref, reference.Value, conan_module.DefaultRevision)
|
||||
|
||||
if err := deleteRecipeOrPackage(ctx, rref, false, pref, true); err != nil {
|
||||
if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func deleteRecipeOrPackage(apictx *context.Context, rref *conan_module.RecipeReference, ignoreRecipeRevision bool, pref *conan_module.PackageReference, ignorePackageRevision bool) error {
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, apictx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pd, err := packages_model.GetPackageDescriptor(ctx, pv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filter := map[string]string{
|
||||
conan_module.PropertyRecipeUser: rref.User,
|
||||
conan_module.PropertyRecipeChannel: rref.Channel,
|
||||
}
|
||||
if !ignoreRecipeRevision {
|
||||
filter[conan_module.PropertyRecipeRevision] = rref.RevisionOrDefault()
|
||||
}
|
||||
if pref != nil {
|
||||
filter[conan_module.PropertyPackageReference] = pref.Reference
|
||||
if !ignorePackageRevision {
|
||||
filter[conan_module.PropertyPackageRevision] = pref.RevisionOrDefault()
|
||||
}
|
||||
}
|
||||
|
||||
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
||||
VersionID: pv.ID,
|
||||
Properties: filter,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(pfs) == 0 {
|
||||
return conan_model.ErrPackageReferenceNotExist
|
||||
}
|
||||
|
||||
for _, pf := range pfs {
|
||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := packages_model.DeleteFileByID(ctx, pf.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
versionDeleted := false
|
||||
has, err := packages_model.HasVersionFileReferences(ctx, pv.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
versionDeleted = true
|
||||
|
||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeVersion, pv.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := packages_model.DeleteVersionByID(ctx, pv.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := committer.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if versionDeleted {
|
||||
notification.NotifyPackageDelete(apictx.Doer, pd)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListRecipeRevisions gets a list of all recipe revisions
|
||||
func ListRecipeRevisions(ctx *context.Context) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
|
||||
revisions, err := conan_model.GetRecipeRevisions(ctx, ctx.Package.Owner.ID, rref)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
listRevisions(ctx, revisions)
|
||||
}
|
||||
|
||||
// ListPackageRevisions gets a list of all package revisions
|
||||
func ListPackageRevisions(ctx *context.Context) {
|
||||
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
||||
|
||||
revisions, err := conan_model.GetPackageRevisions(ctx, ctx.Package.Owner.ID, pref)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
listRevisions(ctx, revisions)
|
||||
}
|
||||
|
||||
type revisionInfo struct {
|
||||
Revision string `json:"revision"`
|
||||
Time time.Time `json:"time"`
|
||||
}
|
||||
|
||||
func listRevisions(ctx *context.Context, revisions []*conan_model.PropertyValue) {
|
||||
if len(revisions) == 0 {
|
||||
apiError(ctx, http.StatusNotFound, conan_model.ErrRecipeReferenceNotExist)
|
||||
return
|
||||
}
|
||||
|
||||
type RevisionList struct {
|
||||
Revisions []*revisionInfo `json:"revisions"`
|
||||
}
|
||||
|
||||
revs := make([]*revisionInfo, 0, len(revisions))
|
||||
for _, rev := range revisions {
|
||||
revs = append(revs, &revisionInfo{Revision: rev.Value, Time: time.Unix(int64(rev.CreatedUnix), 0)})
|
||||
}
|
||||
|
||||
jsonResponse(ctx, http.StatusOK, &RevisionList{revs})
|
||||
}
|
||||
|
||||
// LatestRecipeRevision gets the latest recipe revision
|
||||
func LatestRecipeRevision(ctx *context.Context) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
|
||||
revision, err := conan_model.GetLastRecipeRevision(ctx, ctx.Package.Owner.ID, rref)
|
||||
if err != nil {
|
||||
if err == conan_model.ErrRecipeReferenceNotExist || err == conan_model.ErrPackageReferenceNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
jsonResponse(ctx, http.StatusOK, &revisionInfo{Revision: revision.Value, Time: time.Unix(int64(revision.CreatedUnix), 0)})
|
||||
}
|
||||
|
||||
// LatestPackageRevision gets the latest package revision
|
||||
func LatestPackageRevision(ctx *context.Context) {
|
||||
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
||||
|
||||
revision, err := conan_model.GetLastPackageRevision(ctx, ctx.Package.Owner.ID, pref)
|
||||
if err != nil {
|
||||
if err == conan_model.ErrRecipeReferenceNotExist || err == conan_model.ErrPackageReferenceNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
jsonResponse(ctx, http.StatusOK, &revisionInfo{Revision: revision.Value, Time: time.Unix(int64(revision.CreatedUnix), 0)})
|
||||
}
|
||||
|
||||
// ListRecipeRevisionFiles gets a list of all recipe revision files
|
||||
func ListRecipeRevisionFiles(ctx *context.Context) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
|
||||
listRevisionFiles(ctx, rref.AsKey())
|
||||
}
|
||||
|
||||
// ListPackageRevisionFiles gets a list of all package revision files
|
||||
func ListPackageRevisionFiles(ctx *context.Context) {
|
||||
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
||||
|
||||
listRevisionFiles(ctx, pref.AsKey())
|
||||
}
|
||||
|
||||
func listRevisionFiles(ctx *context.Context, fileKey string) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
||||
VersionID: pv.ID,
|
||||
CompositeKey: fileKey,
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(pfs) == 0 {
|
||||
apiError(ctx, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
files := make(map[string]interface{})
|
||||
for _, pf := range pfs {
|
||||
files[pf.Name] = nil
|
||||
}
|
||||
|
||||
type FileList struct {
|
||||
Files map[string]interface{} `json:"files"`
|
||||
}
|
||||
|
||||
jsonResponse(ctx, http.StatusOK, &FileList{
|
||||
Files: files,
|
||||
})
|
||||
}
|
||||
164
routers/api/packages/conan/search.go
Normal file
164
routers/api/packages/conan/search.go
Normal file
@@ -0,0 +1,164 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package conan
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
conan_model "code.gitea.io/gitea/models/packages/conan"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
conan_module "code.gitea.io/gitea/modules/packages/conan"
|
||||
)
|
||||
|
||||
// SearchResult contains the found recipe names
|
||||
type SearchResult struct {
|
||||
Results []string `json:"results"`
|
||||
}
|
||||
|
||||
// SearchRecipes searches all recipes matching the query
|
||||
func SearchRecipes(ctx *context.Context) {
|
||||
q := ctx.FormTrim("q")
|
||||
|
||||
opts := parseQuery(ctx.Package.Owner, q)
|
||||
|
||||
results, err := conan_model.SearchRecipes(ctx, opts)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonResponse(ctx, http.StatusOK, &SearchResult{
|
||||
Results: results,
|
||||
})
|
||||
}
|
||||
|
||||
// parseQuery creates search options for the given query
|
||||
func parseQuery(owner *user_model.User, query string) *conan_model.RecipeSearchOptions {
|
||||
opts := &conan_model.RecipeSearchOptions{
|
||||
OwnerID: owner.ID,
|
||||
}
|
||||
|
||||
if query != "" {
|
||||
parts := strings.Split(strings.ReplaceAll(query, "@", "/"), "/")
|
||||
|
||||
opts.Name = parts[0]
|
||||
if len(parts) > 1 && parts[1] != "*" {
|
||||
opts.Version = parts[1]
|
||||
}
|
||||
if len(parts) > 2 && parts[2] != "*" {
|
||||
opts.User = parts[2]
|
||||
}
|
||||
if len(parts) > 3 && parts[3] != "*" {
|
||||
opts.Channel = parts[3]
|
||||
}
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// SearchPackagesV1 searches all packages of a recipe (Conan v1 endpoint)
|
||||
func SearchPackagesV1(ctx *context.Context) {
|
||||
searchPackages(ctx, true)
|
||||
}
|
||||
|
||||
// SearchPackagesV2 searches all packages of a recipe (Conan v2 endpoint)
|
||||
func SearchPackagesV2(ctx *context.Context) {
|
||||
searchPackages(ctx, false)
|
||||
}
|
||||
|
||||
func searchPackages(ctx *context.Context, searchAllRevisions bool) {
|
||||
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
||||
|
||||
if !searchAllRevisions && rref.Revision == "" {
|
||||
lastRevision, err := conan_model.GetLastRecipeRevision(ctx, ctx.Package.Owner.ID, rref)
|
||||
if err != nil {
|
||||
if err == conan_model.ErrRecipeReferenceNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
rref = rref.WithRevision(lastRevision.Value)
|
||||
} else {
|
||||
has, err := conan_model.RecipeExists(ctx, ctx.Package.Owner.ID, rref)
|
||||
if err != nil {
|
||||
if err == conan_model.ErrRecipeReferenceNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
apiError(ctx, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
recipeRevisions := []*conan_model.PropertyValue{{Value: rref.Revision}}
|
||||
if searchAllRevisions {
|
||||
var err error
|
||||
recipeRevisions, err = conan_model.GetRecipeRevisions(ctx, ctx.Package.Owner.ID, rref)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := make(map[string]*conan_module.Conaninfo)
|
||||
|
||||
for _, recipeRevision := range recipeRevisions {
|
||||
currentRef := rref
|
||||
if recipeRevision.Value != "" {
|
||||
currentRef = rref.WithRevision(recipeRevision.Value)
|
||||
}
|
||||
packageReferences, err := conan_model.GetPackageReferences(ctx, ctx.Package.Owner.ID, currentRef)
|
||||
if err != nil {
|
||||
if err == conan_model.ErrRecipeReferenceNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, packageReference := range packageReferences {
|
||||
if _, ok := result[packageReference.Value]; ok {
|
||||
continue
|
||||
}
|
||||
pref, _ := conan_module.NewPackageReference(currentRef, packageReference.Value, "")
|
||||
lastPackageRevision, err := conan_model.GetLastPackageRevision(ctx, ctx.Package.Owner.ID, pref)
|
||||
if err != nil {
|
||||
if err == conan_model.ErrPackageReferenceNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
pref = pref.WithRevision(lastPackageRevision.Value)
|
||||
infoRaw, err := conan_model.GetPackageInfo(ctx, ctx.Package.Owner.ID, pref)
|
||||
if err != nil {
|
||||
if err == conan_model.ErrPackageReferenceNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
var info *conan_module.Conaninfo
|
||||
if err := json.Unmarshal([]byte(infoRaw), &info); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
result[pref.Reference] = info
|
||||
}
|
||||
}
|
||||
|
||||
jsonResponse(ctx, http.StatusOK, result)
|
||||
}
|
||||
45
routers/api/packages/container/auth.go
Normal file
45
routers/api/packages/container/auth.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
type Auth struct{}
|
||||
|
||||
func (a *Auth) Name() string {
|
||||
return "container"
|
||||
}
|
||||
|
||||
// Verify extracts the user from the Bearer token
|
||||
// If it's an anonymous session a ghost user is returned
|
||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) *user_model.User {
|
||||
uid, err := packages.ParseAuthorizationToken(req)
|
||||
if err != nil {
|
||||
log.Trace("ParseAuthorizationToken: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if uid == 0 {
|
||||
return nil
|
||||
}
|
||||
if uid == -1 {
|
||||
return user_model.NewGhostUser()
|
||||
}
|
||||
|
||||
u, err := user_model.GetUserByID(uid)
|
||||
if err != nil {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
||||
136
routers/api/packages/container/blob.go
Normal file
136
routers/api/packages/container/blob.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
container_model "code.gitea.io/gitea/models/packages/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
container_module "code.gitea.io/gitea/modules/packages/container"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
// saveAsPackageBlob creates a package blob from an upload
|
||||
// The uploaded blob gets stored in a special upload version to link them to the package/image
|
||||
func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_service.PackageInfo) (*packages_model.PackageBlob, error) {
|
||||
pb := packages_service.NewPackageBlob(hsr)
|
||||
|
||||
exists := false
|
||||
|
||||
contentStore := packages_module.NewContentStore()
|
||||
|
||||
err := db.WithTx(func(ctx context.Context) error {
|
||||
p := &packages_model.Package{
|
||||
OwnerID: pi.Owner.ID,
|
||||
Type: packages_model.TypeContainer,
|
||||
Name: strings.ToLower(pi.Name),
|
||||
LowerName: strings.ToLower(pi.Name),
|
||||
}
|
||||
var err error
|
||||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
|
||||
if err != packages_model.ErrDuplicatePackage {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pv := &packages_model.PackageVersion{
|
||||
PackageID: p.ID,
|
||||
CreatorID: pi.Owner.ID,
|
||||
Version: container_model.UploadVersion,
|
||||
LowerVersion: container_model.UploadVersion,
|
||||
IsInternal: true,
|
||||
MetadataJSON: "null",
|
||||
}
|
||||
if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil {
|
||||
if err != packages_model.ErrDuplicatePackageVersion {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pb, exists, err = packages_model.GetOrInsertBlob(ctx, pb)
|
||||
if err != nil {
|
||||
log.Error("Error inserting package blob: %v", err)
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), hsr, hsr.Size()); err != nil {
|
||||
log.Error("Error saving package blob in content store: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
filename := strings.ToLower(fmt.Sprintf("sha256_%s", pb.HashSHA256))
|
||||
|
||||
pf := &packages_model.PackageFile{
|
||||
VersionID: pv.ID,
|
||||
BlobID: pb.ID,
|
||||
Name: filename,
|
||||
LowerName: filename,
|
||||
CompositeKey: packages_model.EmptyFileKey,
|
||||
}
|
||||
if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageFile {
|
||||
return nil
|
||||
}
|
||||
log.Error("Error inserting package file: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, container_module.PropertyDigest, digestFromPackageBlob(pb)); err != nil {
|
||||
log.Error("Error setting package file property: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if !exists {
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pb, nil
|
||||
}
|
||||
|
||||
func deleteBlob(ownerID int64, image, digest string) error {
|
||||
return db.WithTx(func(ctx context.Context) error {
|
||||
pfds, err := container_model.GetContainerBlobs(ctx, &container_model.BlobSearchOptions{
|
||||
OwnerID: ownerID,
|
||||
Image: image,
|
||||
Digest: digest,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range pfds {
|
||||
if err := packages_service.DeletePackageFile(ctx, file.File); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func digestFromHashSummer(h packages_module.HashSummer) string {
|
||||
_, _, hashSHA256, _ := h.Sums()
|
||||
return "sha256:" + hex.EncodeToString(hashSHA256)
|
||||
}
|
||||
|
||||
func digestFromPackageBlob(pb *packages_model.PackageBlob) string {
|
||||
return "sha256:" + pb.HashSHA256
|
||||
}
|
||||
613
routers/api/packages/container/container.go
Normal file
613
routers/api/packages/container/container.go
Normal file
@@ -0,0 +1,613 @@
|
||||
// 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 container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
container_model "code.gitea.io/gitea/models/packages/container"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
container_module "code.gitea.io/gitea/modules/packages/container"
|
||||
"code.gitea.io/gitea/modules/packages/container/oci"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
container_service "code.gitea.io/gitea/services/packages/container"
|
||||
)
|
||||
|
||||
// maximum size of a container manifest
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
|
||||
const maxManifestSize = 10 * 1024 * 1024
|
||||
|
||||
var imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`)
|
||||
|
||||
type containerHeaders struct {
|
||||
Status int
|
||||
ContentDigest string
|
||||
UploadUUID string
|
||||
Range string
|
||||
Location string
|
||||
ContentType string
|
||||
ContentLength int64
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#legacy-docker-support-http-headers
|
||||
func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) {
|
||||
if h.Location != "" {
|
||||
resp.Header().Set("Location", h.Location)
|
||||
}
|
||||
if h.Range != "" {
|
||||
resp.Header().Set("Range", h.Range)
|
||||
}
|
||||
if h.ContentType != "" {
|
||||
resp.Header().Set("Content-Type", h.ContentType)
|
||||
}
|
||||
if h.ContentLength != 0 {
|
||||
resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10))
|
||||
}
|
||||
if h.UploadUUID != "" {
|
||||
resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID)
|
||||
}
|
||||
if h.ContentDigest != "" {
|
||||
resp.Header().Set("Docker-Content-Digest", h.ContentDigest)
|
||||
resp.Header().Set("ETag", fmt.Sprintf(`"%s"`, h.ContentDigest))
|
||||
}
|
||||
resp.Header().Set("Docker-Distribution-Api-Version", "registry/2.0")
|
||||
resp.WriteHeader(h.Status)
|
||||
}
|
||||
|
||||
func jsonResponse(ctx *context.Context, status int, obj interface{}) {
|
||||
setResponseHeaders(ctx.Resp, &containerHeaders{
|
||||
Status: status,
|
||||
ContentType: "application/json",
|
||||
})
|
||||
if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil {
|
||||
log.Error("JSON encode: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func apiError(ctx *context.Context, status int, err error) {
|
||||
helper.LogAndProcessError(ctx, status, err, func(message string) {
|
||||
setResponseHeaders(ctx.Resp, &containerHeaders{
|
||||
Status: status,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes
|
||||
func apiErrorDefined(ctx *context.Context, err *namedError) {
|
||||
type ContainerError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type ContainerErrors struct {
|
||||
Errors []ContainerError `json:"errors"`
|
||||
}
|
||||
|
||||
jsonResponse(ctx, err.StatusCode, ContainerErrors{
|
||||
Errors: []ContainerError{
|
||||
{
|
||||
Code: err.Code,
|
||||
Message: err.Message,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost for anonymous access)
|
||||
func ReqContainerAccess(ctx *context.Context) {
|
||||
if ctx.Doer == nil {
|
||||
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token"`)
|
||||
ctx.Resp.Header().Add("WWW-Authenticate", `Basic`)
|
||||
apiErrorDefined(ctx, errUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyImageName is a middleware which checks if the image name is allowed
|
||||
func VerifyImageName(ctx *context.Context) {
|
||||
if !imageNamePattern.MatchString(ctx.Params("image")) {
|
||||
apiErrorDefined(ctx, errNameInvalid)
|
||||
}
|
||||
}
|
||||
|
||||
// DetermineSupport is used to test if the registry supports OCI
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#determining-support
|
||||
func DetermineSupport(ctx *context.Context) {
|
||||
setResponseHeaders(ctx.Resp, &containerHeaders{
|
||||
Status: http.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// Authenticate creates a token for the current user
|
||||
// If the current user is anonymous, the ghost user is used
|
||||
func Authenticate(ctx *context.Context) {
|
||||
u := ctx.Doer
|
||||
if u == nil {
|
||||
u = user_model.NewGhostUser()
|
||||
}
|
||||
|
||||
token, err := packages_service.CreateAuthorizationToken(u)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]string{
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
|
||||
func InitiateUploadBlob(ctx *context.Context) {
|
||||
image := ctx.Params("image")
|
||||
|
||||
mount := ctx.FormTrim("mount")
|
||||
from := ctx.FormTrim("from")
|
||||
if mount != "" {
|
||||
blob, _ := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
|
||||
Image: from,
|
||||
Digest: mount,
|
||||
})
|
||||
if blob != nil {
|
||||
setResponseHeaders(ctx.Resp, &containerHeaders{
|
||||
Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, mount),
|
||||
ContentDigest: mount,
|
||||
Status: http.StatusCreated,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
digest := ctx.FormTrim("digest")
|
||||
if digest != "" {
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body, 32*1024*1024)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer buf.Close()
|
||||
|
||||
if digest != digestFromHashSummer(buf) {
|
||||
apiErrorDefined(ctx, errDigestInvalid)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := saveAsPackageBlob(buf, &packages_service.PackageInfo{Owner: ctx.Package.Owner, Name: image}); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
setResponseHeaders(ctx.Resp, &containerHeaders{
|
||||
Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest),
|
||||
ContentDigest: digest,
|
||||
Status: http.StatusCreated,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
upload, err := packages_model.CreateBlobUpload(ctx)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
setResponseHeaders(ctx.Resp, &containerHeaders{
|
||||
Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID),
|
||||
Range: "0-0",
|
||||
UploadUUID: upload.ID,
|
||||
Status: http.StatusAccepted,
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
|
||||
func UploadBlob(ctx *context.Context) {
|
||||
image := ctx.Params("image")
|
||||
|
||||
uploader, err := container_service.NewBlobUploader(ctx, ctx.Params("uuid"))
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageBlobUploadNotExist {
|
||||
apiErrorDefined(ctx, errBlobUploadUnknown)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer uploader.Close()
|
||||
|
||||
contentRange := ctx.Req.Header.Get("Content-Range")
|
||||
if contentRange != "" {
|
||||
start, end := 0, 0
|
||||
if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil {
|
||||
apiErrorDefined(ctx, errBlobUploadInvalid)
|
||||
return
|
||||
}
|
||||
|
||||
if int64(start) != uploader.Size() {
|
||||
apiErrorDefined(ctx, errBlobUploadInvalid.WithStatusCode(http.StatusRequestedRangeNotSatisfiable))
|
||||
return
|
||||
}
|
||||
} else if uploader.Size() != 0 {
|
||||
apiErrorDefined(ctx, errBlobUploadInvalid.WithMessage("Stream uploads after first write are not allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := uploader.Append(ctx, ctx.Req.Body); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
setResponseHeaders(ctx.Resp, &containerHeaders{
|
||||
Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, uploader.ID),
|
||||
Range: fmt.Sprintf("0-%d", uploader.Size()-1),
|
||||
UploadUUID: uploader.ID,
|
||||
Status: http.StatusAccepted,
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
|
||||
func EndUploadBlob(ctx *context.Context) {
|
||||
image := ctx.Params("image")
|
||||
|
||||
digest := ctx.FormTrim("digest")
|
||||
if digest == "" {
|
||||
apiErrorDefined(ctx, errDigestInvalid)
|
||||
return
|
||||
}
|
||||
|
||||
uploader, err := container_service.NewBlobUploader(ctx, ctx.Params("uuid"))
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageBlobUploadNotExist {
|
||||
apiErrorDefined(ctx, errBlobUploadUnknown)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
close := true
|
||||
defer func() {
|
||||
if close {
|
||||
uploader.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if ctx.Req.Body != nil {
|
||||
if err := uploader.Append(ctx, ctx.Req.Body); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if digest != digestFromHashSummer(uploader) {
|
||||
apiErrorDefined(ctx, errDigestInvalid)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := saveAsPackageBlob(uploader, &packages_service.PackageInfo{Owner: ctx.Package.Owner, Name: image}); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := uploader.Close(); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
close = false
|
||||
|
||||
if err := container_service.RemoveBlobUploadByID(ctx, uploader.ID); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
setResponseHeaders(ctx.Resp, &containerHeaders{
|
||||
Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest),
|
||||
ContentDigest: digest,
|
||||
Status: http.StatusCreated,
|
||||
})
|
||||
}
|
||||
|
||||
func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
|
||||
digest := ctx.Params("digest")
|
||||
|
||||
if !oci.Digest(digest).Validate() {
|
||||
return nil, container_model.ErrContainerBlobNotExist
|
||||
}
|
||||
|
||||
return container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Image: ctx.Params("image"),
|
||||
Digest: digest,
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
|
||||
func HeadBlob(ctx *context.Context) {
|
||||
blob, err := getBlobFromContext(ctx)
|
||||
if err != nil {
|
||||
if err == container_model.ErrContainerBlobNotExist {
|
||||
apiErrorDefined(ctx, errBlobUnknown)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setResponseHeaders(ctx.Resp, &containerHeaders{
|
||||
ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest),
|
||||
ContentLength: blob.Blob.Size,
|
||||
Status: http.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-blobs
|
||||
func GetBlob(ctx *context.Context) {
|
||||
blob, err := getBlobFromContext(ctx)
|
||||
if err != nil {
|
||||
if err == container_model.ErrContainerBlobNotExist {
|
||||
apiErrorDefined(ctx, errBlobUnknown)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
s, _, err := packages_service.GetPackageFileStream(ctx, blob.File)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
setResponseHeaders(ctx.Resp, &containerHeaders{
|
||||
ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest),
|
||||
ContentType: blob.Properties.GetByName(container_module.PropertyMediaType),
|
||||
ContentLength: blob.Blob.Size,
|
||||
Status: http.StatusOK,
|
||||
})
|
||||
if _, err := io.Copy(ctx.Resp, s); err != nil {
|
||||
log.Error("Error whilst copying content to response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs
|
||||
func DeleteBlob(ctx *context.Context) {
|
||||
digest := ctx.Params("digest")
|
||||
|
||||
if !oci.Digest(digest).Validate() {
|
||||
apiErrorDefined(ctx, errBlobUnknown)
|
||||
return
|
||||
}
|
||||
|
||||
if err := deleteBlob(ctx.Package.Owner.ID, ctx.Params("image"), digest); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
setResponseHeaders(ctx.Resp, &containerHeaders{
|
||||
Status: http.StatusAccepted,
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
|
||||
func UploadManifest(ctx *context.Context) {
|
||||
reference := ctx.Params("reference")
|
||||
|
||||
mci := &manifestCreationInfo{
|
||||
MediaType: oci.MediaType(ctx.Req.Header.Get("Content-Type")),
|
||||
Owner: ctx.Package.Owner,
|
||||
Creator: ctx.Doer,
|
||||
Image: ctx.Params("image"),
|
||||
Reference: reference,
|
||||
IsTagged: !oci.Digest(reference).Validate(),
|
||||
}
|
||||
|
||||
if mci.IsTagged && !oci.Reference(reference).Validate() {
|
||||
apiErrorDefined(ctx, errManifestInvalid.WithMessage("Tag is invalid"))
|
||||
return
|
||||
}
|
||||
|
||||
maxSize := maxManifestSize + 1
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(&io.LimitedReader{R: ctx.Req.Body, N: int64(maxSize)}, maxSize)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer buf.Close()
|
||||
|
||||
if buf.Size() > maxManifestSize {
|
||||
apiErrorDefined(ctx, errManifestInvalid.WithMessage("Manifest exceeds maximum size").WithStatusCode(http.StatusRequestEntityTooLarge))
|
||||
return
|
||||
}
|
||||
|
||||
digest, err := processManifest(mci, buf)
|
||||
if err != nil {
|
||||
var namedError *namedError
|
||||
if errors.As(err, &namedError) {
|
||||
apiErrorDefined(ctx, namedError)
|
||||
} else if errors.Is(err, container_model.ErrContainerBlobNotExist) {
|
||||
apiErrorDefined(ctx, errBlobUnknown)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setResponseHeaders(ctx.Resp, &containerHeaders{
|
||||
Location: fmt.Sprintf("/v2/%s/%s/manifests/%s", ctx.Package.Owner.LowerName, mci.Image, reference),
|
||||
ContentDigest: digest,
|
||||
Status: http.StatusCreated,
|
||||
})
|
||||
}
|
||||
|
||||
func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
|
||||
reference := ctx.Params("reference")
|
||||
|
||||
opts := &container_model.BlobSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Image: ctx.Params("image"),
|
||||
IsManifest: true,
|
||||
}
|
||||
if oci.Digest(reference).Validate() {
|
||||
opts.Digest = reference
|
||||
} else if oci.Reference(reference).Validate() {
|
||||
opts.Tag = reference
|
||||
} else {
|
||||
return nil, container_model.ErrContainerBlobNotExist
|
||||
}
|
||||
|
||||
return container_model.GetContainerBlob(ctx, opts)
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
|
||||
func HeadManifest(ctx *context.Context) {
|
||||
manifest, err := getManifestFromContext(ctx)
|
||||
if err != nil {
|
||||
if err == container_model.ErrContainerBlobNotExist {
|
||||
apiErrorDefined(ctx, errManifestUnknown)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setResponseHeaders(ctx.Resp, &containerHeaders{
|
||||
ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest),
|
||||
ContentType: manifest.Properties.GetByName(container_module.PropertyMediaType),
|
||||
ContentLength: manifest.Blob.Size,
|
||||
Status: http.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests
|
||||
func GetManifest(ctx *context.Context) {
|
||||
manifest, err := getManifestFromContext(ctx)
|
||||
if err != nil {
|
||||
if err == container_model.ErrContainerBlobNotExist {
|
||||
apiErrorDefined(ctx, errManifestUnknown)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
s, _, err := packages_service.GetPackageFileStream(ctx, manifest.File)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
setResponseHeaders(ctx.Resp, &containerHeaders{
|
||||
ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest),
|
||||
ContentType: manifest.Properties.GetByName(container_module.PropertyMediaType),
|
||||
ContentLength: manifest.Blob.Size,
|
||||
Status: http.StatusOK,
|
||||
})
|
||||
if _, err := io.Copy(ctx.Resp, s); err != nil {
|
||||
log.Error("Error whilst copying content to response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-tags
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-manifests
|
||||
func DeleteManifest(ctx *context.Context) {
|
||||
reference := ctx.Params("reference")
|
||||
|
||||
opts := &container_model.BlobSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Image: ctx.Params("image"),
|
||||
IsManifest: true,
|
||||
}
|
||||
if oci.Digest(reference).Validate() {
|
||||
opts.Digest = reference
|
||||
} else if oci.Reference(reference).Validate() {
|
||||
opts.Tag = reference
|
||||
} else {
|
||||
apiErrorDefined(ctx, errManifestUnknown)
|
||||
return
|
||||
}
|
||||
|
||||
pvs, err := container_model.GetManifestVersions(ctx, opts)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(pvs) == 0 {
|
||||
apiErrorDefined(ctx, errManifestUnknown)
|
||||
return
|
||||
}
|
||||
|
||||
for _, pv := range pvs {
|
||||
if err := packages_service.RemovePackageVersion(ctx.Doer, pv); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setResponseHeaders(ctx.Resp, &containerHeaders{
|
||||
Status: http.StatusAccepted,
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery
|
||||
func GetTagList(ctx *context.Context) {
|
||||
image := ctx.Params("image")
|
||||
|
||||
if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil {
|
||||
if err == packages_model.ErrPackageNotExist {
|
||||
apiErrorDefined(ctx, errNameUnknown)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
n := -1
|
||||
if ctx.FormTrim("n") != "" {
|
||||
n = ctx.FormInt("n")
|
||||
}
|
||||
last := ctx.FormTrim("last")
|
||||
|
||||
tags, err := container_model.GetImageTags(ctx, ctx.Package.Owner.ID, image, n, last)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
type TagList struct {
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
if len(tags) > 0 {
|
||||
v := url.Values{}
|
||||
if n > 0 {
|
||||
v.Add("n", strconv.Itoa(n))
|
||||
}
|
||||
v.Add("last", tags[len(tags)-1])
|
||||
|
||||
ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/%s/%s/tags/list?%s>; rel="next"`, ctx.Package.Owner.LowerName, image, v.Encode()))
|
||||
}
|
||||
|
||||
jsonResponse(ctx, http.StatusOK, TagList{
|
||||
Name: strings.ToLower(ctx.Package.Owner.LowerName + "/" + image),
|
||||
Tags: tags,
|
||||
})
|
||||
}
|
||||
53
routers/api/packages/container/errors.go
Normal file
53
routers/api/packages/container/errors.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes
|
||||
var (
|
||||
errBlobUnknown = &namedError{Code: "BLOB_UNKNOWN", StatusCode: http.StatusNotFound}
|
||||
errBlobUploadInvalid = &namedError{Code: "BLOB_UPLOAD_INVALID", StatusCode: http.StatusBadRequest}
|
||||
errBlobUploadUnknown = &namedError{Code: "BLOB_UPLOAD_UNKNOWN", StatusCode: http.StatusNotFound}
|
||||
errDigestInvalid = &namedError{Code: "DIGEST_INVALID", StatusCode: http.StatusBadRequest}
|
||||
errManifestBlobUnknown = &namedError{Code: "MANIFEST_BLOB_UNKNOWN", StatusCode: http.StatusNotFound}
|
||||
errManifestInvalid = &namedError{Code: "MANIFEST_INVALID", StatusCode: http.StatusBadRequest}
|
||||
errManifestUnknown = &namedError{Code: "MANIFEST_UNKNOWN", StatusCode: http.StatusNotFound}
|
||||
errNameInvalid = &namedError{Code: "NAME_INVALID", StatusCode: http.StatusBadRequest}
|
||||
errNameUnknown = &namedError{Code: "NAME_UNKNOWN", StatusCode: http.StatusNotFound}
|
||||
errSizeInvalid = &namedError{Code: "SIZE_INVALID", StatusCode: http.StatusBadRequest}
|
||||
errUnauthorized = &namedError{Code: "UNAUTHORIZED", StatusCode: http.StatusUnauthorized}
|
||||
errUnsupported = &namedError{Code: "UNSUPPORTED", StatusCode: http.StatusNotImplemented}
|
||||
)
|
||||
|
||||
type namedError struct {
|
||||
Code string
|
||||
StatusCode int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *namedError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// WithMessage creates a new instance of the error with a different message
|
||||
func (e *namedError) WithMessage(message string) *namedError {
|
||||
return &namedError{
|
||||
Code: e.Code,
|
||||
StatusCode: e.StatusCode,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// WithStatusCode creates a new instance of the error with a different status code
|
||||
func (e *namedError) WithStatusCode(statusCode int) *namedError {
|
||||
return &namedError{
|
||||
Code: e.Code,
|
||||
StatusCode: statusCode,
|
||||
Message: e.Message,
|
||||
}
|
||||
}
|
||||
412
routers/api/packages/container/manifest.go
Normal file
412
routers/api/packages/container/manifest.go
Normal file
@@ -0,0 +1,412 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
container_model "code.gitea.io/gitea/models/packages/container"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
container_module "code.gitea.io/gitea/modules/packages/container"
|
||||
"code.gitea.io/gitea/modules/packages/container/oci"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
// manifestCreationInfo describes a manifest to create
|
||||
type manifestCreationInfo struct {
|
||||
MediaType oci.MediaType
|
||||
Owner *user_model.User
|
||||
Creator *user_model.User
|
||||
Image string
|
||||
Reference string
|
||||
IsTagged bool
|
||||
Properties map[string]string
|
||||
}
|
||||
|
||||
func processManifest(mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) {
|
||||
var schema oci.SchemaMediaBase
|
||||
if err := json.NewDecoder(buf).Decode(&schema); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if schema.SchemaVersion != 2 {
|
||||
return "", errUnsupported.WithMessage("Schema version is not supported")
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !mci.MediaType.IsValid() {
|
||||
mci.MediaType = schema.MediaType
|
||||
if !mci.MediaType.IsValid() {
|
||||
return "", errManifestInvalid.WithMessage("MediaType not recognized")
|
||||
}
|
||||
}
|
||||
|
||||
if mci.MediaType.IsImageManifest() {
|
||||
d, err := processImageManifest(mci, buf)
|
||||
return d, err
|
||||
} else if mci.MediaType.IsImageIndex() {
|
||||
d, err := processImageManifestIndex(mci, buf)
|
||||
return d, err
|
||||
}
|
||||
return "", errManifestInvalid
|
||||
}
|
||||
|
||||
func processImageManifest(mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) {
|
||||
manifestDigest := ""
|
||||
|
||||
err := func() error {
|
||||
var manifest oci.Manifest
|
||||
if err := json.NewDecoder(buf).Decode(&manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
configDescriptor, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
|
||||
OwnerID: mci.Owner.ID,
|
||||
Image: mci.Image,
|
||||
Digest: string(manifest.Config.Digest),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configReader, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(configDescriptor.Blob.HashSHA256))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer configReader.Close()
|
||||
|
||||
metadata, err := container_module.ParseImageConfig(manifest.Config.MediaType, configReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
blobReferences := make([]*blobReference, 0, 1+len(manifest.Layers))
|
||||
|
||||
blobReferences = append(blobReferences, &blobReference{
|
||||
Digest: manifest.Config.Digest,
|
||||
MediaType: manifest.Config.MediaType,
|
||||
File: configDescriptor,
|
||||
ExpectedSize: manifest.Config.Size,
|
||||
})
|
||||
|
||||
for _, layer := range manifest.Layers {
|
||||
pfd, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
|
||||
OwnerID: mci.Owner.ID,
|
||||
Image: mci.Image,
|
||||
Digest: string(layer.Digest),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
blobReferences = append(blobReferences, &blobReference{
|
||||
Digest: layer.Digest,
|
||||
MediaType: layer.MediaType,
|
||||
File: pfd,
|
||||
ExpectedSize: layer.Size,
|
||||
})
|
||||
}
|
||||
|
||||
pv, err := createPackageAndVersion(ctx, mci, metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uploadVersion, err := packages_model.GetInternalVersionByNameAndVersion(ctx, mci.Owner.ID, packages_model.TypeContainer, mci.Image, container_model.UploadVersion)
|
||||
if err != nil && err != packages_model.ErrPackageNotExist {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ref := range blobReferences {
|
||||
if err := createFileFromBlobReference(ctx, pv, uploadVersion, ref); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pb, created, digest, err := createManifestBlob(ctx, mci, pv, buf)
|
||||
removeBlob := false
|
||||
defer func() {
|
||||
if removeBlob {
|
||||
contentStore := packages_module.NewContentStore()
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
removeBlob = created
|
||||
return err
|
||||
}
|
||||
|
||||
if err := committer.Commit(); err != nil {
|
||||
removeBlob = created
|
||||
return err
|
||||
}
|
||||
|
||||
manifestDigest = digest
|
||||
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return manifestDigest, nil
|
||||
}
|
||||
|
||||
func processImageManifestIndex(mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) {
|
||||
manifestDigest := ""
|
||||
|
||||
err := func() error {
|
||||
var index oci.Index
|
||||
if err := json.NewDecoder(buf).Decode(&index); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
metadata := &container_module.Metadata{
|
||||
Type: container_module.TypeOCI,
|
||||
MultiArch: make(map[string]string),
|
||||
}
|
||||
|
||||
for _, manifest := range index.Manifests {
|
||||
if !manifest.MediaType.IsImageManifest() {
|
||||
return errManifestInvalid
|
||||
}
|
||||
|
||||
platform := container_module.DefaultPlatform
|
||||
if manifest.Platform != nil {
|
||||
platform = fmt.Sprintf("%s/%s", manifest.Platform.OS, manifest.Platform.Architecture)
|
||||
if manifest.Platform.Variant != "" {
|
||||
platform = fmt.Sprintf("%s/%s", platform, manifest.Platform.Variant)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
|
||||
OwnerID: mci.Owner.ID,
|
||||
Image: mci.Image,
|
||||
Digest: string(manifest.Digest),
|
||||
IsManifest: true,
|
||||
})
|
||||
if err != nil {
|
||||
if err == container_model.ErrContainerBlobNotExist {
|
||||
return errManifestBlobUnknown
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
metadata.MultiArch[platform] = string(manifest.Digest)
|
||||
}
|
||||
|
||||
pv, err := createPackageAndVersion(ctx, mci, metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pb, created, digest, err := createManifestBlob(ctx, mci, pv, buf)
|
||||
removeBlob := false
|
||||
defer func() {
|
||||
if removeBlob {
|
||||
contentStore := packages_module.NewContentStore()
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
removeBlob = created
|
||||
return err
|
||||
}
|
||||
|
||||
if err := committer.Commit(); err != nil {
|
||||
removeBlob = created
|
||||
return err
|
||||
}
|
||||
|
||||
manifestDigest = digest
|
||||
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return manifestDigest, nil
|
||||
}
|
||||
|
||||
func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, metadata *container_module.Metadata) (*packages_model.PackageVersion, error) {
|
||||
p := &packages_model.Package{
|
||||
OwnerID: mci.Owner.ID,
|
||||
Type: packages_model.TypeContainer,
|
||||
Name: strings.ToLower(mci.Image),
|
||||
LowerName: strings.ToLower(mci.Image),
|
||||
}
|
||||
var err error
|
||||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
|
||||
if err != packages_model.ErrDuplicatePackage {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
metadata.IsTagged = mci.IsTagged
|
||||
|
||||
metadataJSON, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_pv := &packages_model.PackageVersion{
|
||||
PackageID: p.ID,
|
||||
CreatorID: mci.Creator.ID,
|
||||
Version: strings.ToLower(mci.Reference),
|
||||
LowerVersion: strings.ToLower(mci.Reference),
|
||||
MetadataJSON: string(metadataJSON),
|
||||
}
|
||||
var pv *packages_model.PackageVersion
|
||||
if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageVersion {
|
||||
if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if mci.IsTagged {
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged, ""); err != nil {
|
||||
log.Error("Error setting package version property: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, digest := range metadata.MultiArch {
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, digest); err != nil {
|
||||
log.Error("Error setting package version property: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return pv, nil
|
||||
}
|
||||
|
||||
type blobReference struct {
|
||||
Digest oci.Digest
|
||||
MediaType oci.MediaType
|
||||
Name string
|
||||
File *packages_model.PackageFileDescriptor
|
||||
ExpectedSize int64
|
||||
IsLead bool
|
||||
}
|
||||
|
||||
func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *packages_model.PackageVersion, ref *blobReference) error {
|
||||
if ref.File.Blob.Size != ref.ExpectedSize {
|
||||
return errSizeInvalid
|
||||
}
|
||||
|
||||
if ref.Name == "" {
|
||||
ref.Name = strings.ToLower(fmt.Sprintf("sha256_%s", ref.File.Blob.HashSHA256))
|
||||
}
|
||||
|
||||
pf := &packages_model.PackageFile{
|
||||
VersionID: pv.ID,
|
||||
BlobID: ref.File.Blob.ID,
|
||||
Name: ref.Name,
|
||||
LowerName: ref.Name,
|
||||
IsLead: ref.IsLead,
|
||||
}
|
||||
var err error
|
||||
if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageFile {
|
||||
// Skip this blob because the manifest contains the same filesystem layer multiple times.
|
||||
return nil
|
||||
}
|
||||
log.Error("Error inserting package file: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
props := map[string]string{
|
||||
container_module.PropertyMediaType: string(ref.MediaType),
|
||||
container_module.PropertyDigest: string(ref.Digest),
|
||||
}
|
||||
for name, value := range props {
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, name, value); err != nil {
|
||||
log.Error("Error setting package file property: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the file from the blob upload version
|
||||
if uploadVersion != nil && ref.File.File != nil && uploadVersion.ID == ref.File.File.VersionID {
|
||||
if err := packages_service.DeletePackageFile(ctx, ref.File.File); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *packages_model.PackageVersion, buf *packages_module.HashedBuffer) (*packages_model.PackageBlob, bool, string, error) {
|
||||
pb, exists, err := packages_model.GetOrInsertBlob(ctx, packages_service.NewPackageBlob(buf))
|
||||
if err != nil {
|
||||
log.Error("Error inserting package blob: %v", err)
|
||||
return nil, false, "", err
|
||||
}
|
||||
if !exists {
|
||||
contentStore := packages_module.NewContentStore()
|
||||
if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), buf, buf.Size()); err != nil {
|
||||
log.Error("Error saving package blob in content store: %v", err)
|
||||
return nil, false, "", err
|
||||
}
|
||||
}
|
||||
|
||||
manifestDigest := digestFromHashSummer(buf)
|
||||
err = createFileFromBlobReference(ctx, pv, nil, &blobReference{
|
||||
Digest: oci.Digest(manifestDigest),
|
||||
MediaType: mci.MediaType,
|
||||
Name: container_model.ManifestFilename,
|
||||
File: &packages_model.PackageFileDescriptor{Blob: pb},
|
||||
ExpectedSize: pb.Size,
|
||||
IsLead: true,
|
||||
})
|
||||
|
||||
return pb, !exists, manifestDigest, err
|
||||
}
|
||||
166
routers/api/packages/generic/generic.go
Normal file
166
routers/api/packages/generic/generic.go
Normal file
@@ -0,0 +1,166 @@
|
||||
// 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"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
var (
|
||||
packageNameRegex = regexp.MustCompile(`\A[A-Za-z0-9\.\_\-\+]+\z`)
|
||||
filenameRegex = packageNameRegex
|
||||
)
|
||||
|
||||
func apiError(ctx *context.Context, status int, obj interface{}) {
|
||||
helper.LogAndProcessError(ctx, status, obj, func(message string) {
|
||||
ctx.PlainText(status, message)
|
||||
})
|
||||
}
|
||||
|
||||
// DownloadPackageFile serves the specific generic package.
|
||||
func DownloadPackageFile(ctx *context.Context) {
|
||||
packageName, packageVersion, filename, err := sanitizeParameters(ctx)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
s, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
|
||||
ctx,
|
||||
&packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeGeneric,
|
||||
Name: packageName,
|
||||
Version: packageVersion,
|
||||
},
|
||||
&packages_service.PackageFileInfo{
|
||||
Filename: filename,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
ctx.ServeStream(s, pf.Name)
|
||||
}
|
||||
|
||||
// UploadPackage uploads the specific generic package.
|
||||
// Duplicated packages get rejected.
|
||||
func UploadPackage(ctx *context.Context) {
|
||||
packageName, packageVersion, filename, err := sanitizeParameters(ctx)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
upload, close, err := ctx.UploadStream()
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if close {
|
||||
defer upload.Close()
|
||||
}
|
||||
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(upload, 32*1024*1024)
|
||||
if err != nil {
|
||||
log.Error("Error creating hashed buffer: %v", err)
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer buf.Close()
|
||||
|
||||
_, _, err = packages_service.CreatePackageAndAddFile(
|
||||
&packages_service.PackageCreationInfo{
|
||||
PackageInfo: packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeGeneric,
|
||||
Name: packageName,
|
||||
Version: packageVersion,
|
||||
},
|
||||
SemverCompatible: true,
|
||||
Creator: ctx.Doer,
|
||||
},
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: filename,
|
||||
},
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageVersion {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
// DeletePackage deletes the specific generic package.
|
||||
func DeletePackage(ctx *context.Context) {
|
||||
packageName, packageVersion, _, err := sanitizeParameters(ctx)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = packages_service.RemovePackageVersionByNameAndVersion(
|
||||
ctx.Doer,
|
||||
&packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeGeneric,
|
||||
Name: packageName,
|
||||
Version: packageVersion,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func sanitizeParameters(ctx *context.Context) (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")
|
||||
}
|
||||
|
||||
v, err := version.NewSemver(ctx.Params("packageversion"))
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
return packageName, v.String(), filename, nil
|
||||
}
|
||||
205
routers/api/packages/helm/helm.go
Normal file
205
routers/api/packages/helm/helm.go
Normal file
@@ -0,0 +1,205 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package helm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
helm_module "code.gitea.io/gitea/modules/packages/helm"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func apiError(ctx *context.Context, status int, obj interface{}) {
|
||||
helper.LogAndProcessError(ctx, status, obj, func(message string) {
|
||||
type Error struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
ctx.JSON(status, Error{
|
||||
Error: message,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Index generates the Helm charts index
|
||||
func Index(ctx *context.Context) {
|
||||
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeHelm,
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
baseURL := setting.AppURL + "api/packages/" + url.PathEscape(ctx.Package.Owner.Name) + "/helm"
|
||||
|
||||
type ChartVersion struct {
|
||||
helm_module.Metadata `yaml:",inline"`
|
||||
URLs []string `yaml:"urls"`
|
||||
Created time.Time `yaml:"created,omitempty"`
|
||||
Removed bool `yaml:"removed,omitempty"`
|
||||
Digest string `yaml:"digest,omitempty"`
|
||||
}
|
||||
|
||||
type ServerInfo struct {
|
||||
ContextPath string `yaml:"contextPath,omitempty"`
|
||||
}
|
||||
|
||||
type Index struct {
|
||||
APIVersion string `yaml:"apiVersion"`
|
||||
Entries map[string][]*ChartVersion `yaml:"entries"`
|
||||
Generated time.Time `yaml:"generated,omitempty"`
|
||||
ServerInfo *ServerInfo `yaml:"serverInfo,omitempty"`
|
||||
}
|
||||
|
||||
entries := make(map[string][]*ChartVersion)
|
||||
for _, pv := range pvs {
|
||||
metadata := &helm_module.Metadata{}
|
||||
if err := json.Unmarshal([]byte(pv.MetadataJSON), &metadata); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
entries[metadata.Name] = append(entries[metadata.Name], &ChartVersion{
|
||||
Metadata: *metadata,
|
||||
Created: pv.CreatedUnix.AsTime(),
|
||||
URLs: []string{fmt.Sprintf("%s/%s", baseURL, url.PathEscape(createFilename(metadata)))},
|
||||
})
|
||||
}
|
||||
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
if err := yaml.NewEncoder(ctx.Resp).Encode(&Index{
|
||||
APIVersion: "v1",
|
||||
Entries: entries,
|
||||
Generated: time.Now(),
|
||||
ServerInfo: &ServerInfo{
|
||||
ContextPath: setting.AppSubURL + "/api/packages/" + url.PathEscape(ctx.Package.Owner.Name) + "/helm",
|
||||
},
|
||||
}); err != nil {
|
||||
log.Error("YAML encode failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// DownloadPackageFile serves the content of a package
|
||||
func DownloadPackageFile(ctx *context.Context) {
|
||||
filename := ctx.Params("filename")
|
||||
|
||||
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeHelm,
|
||||
Name: packages_model.SearchValue{
|
||||
ExactMatch: true,
|
||||
Value: ctx.Params("package"),
|
||||
},
|
||||
HasFileWithName: filename,
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(pvs) != 1 {
|
||||
apiError(ctx, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
s, pf, err := packages_service.GetFileStreamByPackageVersion(
|
||||
ctx,
|
||||
pvs[0],
|
||||
&packages_service.PackageFileInfo{
|
||||
Filename: filename,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageFileNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
ctx.ServeStream(s, pf.Name)
|
||||
}
|
||||
|
||||
// UploadPackage creates a new package
|
||||
func UploadPackage(ctx *context.Context) {
|
||||
upload, needToClose, err := ctx.UploadStream()
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if needToClose {
|
||||
defer upload.Close()
|
||||
}
|
||||
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(upload, 32*1024*1024)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer buf.Close()
|
||||
|
||||
metadata, err := helm_module.ParseChartArchive(buf)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
|
||||
&packages_service.PackageCreationInfo{
|
||||
PackageInfo: packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeHelm,
|
||||
Name: metadata.Name,
|
||||
Version: metadata.Version,
|
||||
},
|
||||
SemverCompatible: true,
|
||||
Creator: ctx.Doer,
|
||||
Metadata: metadata,
|
||||
},
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: createFilename(metadata),
|
||||
},
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
OverwriteExisting: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageVersion {
|
||||
apiError(ctx, http.StatusConflict, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
func createFilename(metadata *helm_module.Metadata) string {
|
||||
return strings.ToLower(fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version))
|
||||
}
|
||||
38
routers/api/packages/helper/helper.go
Normal file
38
routers/api/packages/helper/helper.go
Normal file
@@ -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 helper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// LogAndProcessError logs an error and calls a custom callback with the processed error message.
|
||||
// If the error is an InternalServerError the message is stripped if the user is not an admin.
|
||||
func LogAndProcessError(ctx *context.Context, status int, obj interface{}, cb func(string)) {
|
||||
var message string
|
||||
if err, ok := obj.(error); ok {
|
||||
message = err.Error()
|
||||
} else if obj != nil {
|
||||
message = fmt.Sprintf("%s", obj)
|
||||
}
|
||||
if status == http.StatusInternalServerError {
|
||||
log.ErrorWithSkip(1, message)
|
||||
|
||||
if setting.IsProd && (ctx.Doer == nil || !ctx.Doer.IsAdmin) {
|
||||
message = ""
|
||||
}
|
||||
} else {
|
||||
log.Debug(message)
|
||||
}
|
||||
|
||||
if cb != nil {
|
||||
cb(message)
|
||||
}
|
||||
}
|
||||
56
routers/api/packages/maven/api.go
Normal file
56
routers/api/packages/maven/api.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
maven_module "code.gitea.io/gitea/modules/packages/maven"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
Release string `xml:"versioning>release,omitempty"`
|
||||
Latest string `xml:"versioning>latest"`
|
||||
Version []string `xml:"versioning>versions>version"`
|
||||
}
|
||||
|
||||
func createMetadataResponse(pds []*packages_model.PackageDescriptor) *MetadataResponse {
|
||||
sort.Slice(pds, func(i, j int) bool {
|
||||
// Maven and Gradle order packages by their creation timestamp and not by their version string
|
||||
return pds[i].Version.CreatedUnix < pds[j].Version.CreatedUnix
|
||||
})
|
||||
|
||||
var release *packages_model.PackageDescriptor
|
||||
|
||||
versions := make([]string, 0, len(pds))
|
||||
for _, pd := range pds {
|
||||
if !strings.HasSuffix(pd.Version.Version, "-SNAPSHOT") {
|
||||
release = pd
|
||||
}
|
||||
versions = append(versions, pd.Version.Version)
|
||||
}
|
||||
|
||||
latest := pds[len(pds)-1]
|
||||
|
||||
metadata := latest.Metadata.(*maven_module.Metadata)
|
||||
|
||||
resp := &MetadataResponse{
|
||||
GroupID: metadata.GroupID,
|
||||
ArtifactID: metadata.ArtifactID,
|
||||
Latest: latest.Version.Version,
|
||||
Version: versions,
|
||||
}
|
||||
if release != nil {
|
||||
resp.Release = release.Version.Version
|
||||
}
|
||||
return resp
|
||||
}
|
||||
378
routers/api/packages/maven/maven.go
Normal file
378
routers/api/packages/maven/maven.go
Normal file
@@ -0,0 +1,378 @@
|
||||
// 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"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
maven_module "code.gitea.io/gitea/modules/packages/maven"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
const (
|
||||
mavenMetadataFile = "maven-metadata.xml"
|
||||
extensionMD5 = ".md5"
|
||||
extensionSHA1 = ".sha1"
|
||||
extensionSHA256 = ".sha256"
|
||||
extensionSHA512 = ".sha512"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidParameters = errors.New("request parameters are invalid")
|
||||
illegalCharacters = regexp.MustCompile(`[\\/:"<>|?\*]`)
|
||||
)
|
||||
|
||||
func apiError(ctx *context.Context, status int, obj interface{}) {
|
||||
helper.LogAndProcessError(ctx, status, obj, func(message string) {
|
||||
ctx.PlainText(status, message)
|
||||
})
|
||||
}
|
||||
|
||||
// DownloadPackageFile serves the content of a package
|
||||
func DownloadPackageFile(ctx *context.Context) {
|
||||
params, err := extractPathParameters(ctx)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
if params.IsMeta && params.Version == "" {
|
||||
serveMavenMetadata(ctx, params)
|
||||
} else {
|
||||
servePackageFile(ctx, params)
|
||||
}
|
||||
}
|
||||
|
||||
func serveMavenMetadata(ctx *context.Context, params parameters) {
|
||||
// /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512]
|
||||
|
||||
packageName := params.GroupID + "-" + params.ArtifactID
|
||||
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(pvs) == 0 {
|
||||
apiError(ctx, http.StatusNotFound, packages_model.ErrPackageNotExist)
|
||||
return
|
||||
}
|
||||
|
||||
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
xmlMetadata, err := xml.Marshal(createMetadataResponse(pds))
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
xmlMetadataWithHeader := append([]byte(xml.Header), xmlMetadata...)
|
||||
|
||||
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, fmt.Sprintf("%x", hash))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.PlainTextBytes(http.StatusOK, xmlMetadataWithHeader)
|
||||
}
|
||||
|
||||
func servePackageFile(ctx *context.Context, params parameters) {
|
||||
packageName := params.GroupID + "-" + params.ArtifactID
|
||||
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName, params.Version)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
filename := params.Filename
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
if isChecksumExtension(ext) {
|
||||
filename = filename[:len(filename)-len(ext)]
|
||||
}
|
||||
|
||||
pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, filename, packages_model.EmptyFileKey)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageFileNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if isChecksumExtension(ext) {
|
||||
var hash string
|
||||
switch ext {
|
||||
case extensionMD5:
|
||||
hash = pb.HashMD5
|
||||
case extensionSHA1:
|
||||
hash = pb.HashSHA1
|
||||
case extensionSHA256:
|
||||
hash = pb.HashSHA256
|
||||
case extensionSHA512:
|
||||
hash = pb.HashSHA512
|
||||
}
|
||||
ctx.PlainText(http.StatusOK, hash)
|
||||
return
|
||||
}
|
||||
|
||||
s, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(pb.HashSHA256))
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
if pf.IsLead {
|
||||
if err := packages_model.IncrementDownloadCounter(ctx, pv.ID); err != nil {
|
||||
log.Error("Error incrementing download counter: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
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.Context) {
|
||||
params, err := extractPathParameters(ctx)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Parameters: %+v", params)
|
||||
|
||||
// Ignore the package index /<name>/maven-metadata.xml
|
||||
if params.IsMeta && params.Version == "" {
|
||||
ctx.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
packageName := params.GroupID + "-" + params.ArtifactID
|
||||
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body, 32*1024*1024)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer buf.Close()
|
||||
|
||||
pvci := &packages_service.PackageCreationInfo{
|
||||
PackageInfo: packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeMaven,
|
||||
Name: packageName,
|
||||
Version: params.Version,
|
||||
},
|
||||
SemverCompatible: false,
|
||||
Creator: ctx.Doer,
|
||||
}
|
||||
|
||||
ext := filepath.Ext(params.Filename)
|
||||
|
||||
// Do not upload checksum files but compare the hashes.
|
||||
if isChecksumExtension(ext) {
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, params.Filename[:len(params.Filename)-len(ext)], packages_model.EmptyFileKey)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageFileNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := io.ReadAll(buf)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if (ext == extensionMD5 && pb.HashMD5 != string(hash)) ||
|
||||
(ext == extensionSHA1 && pb.HashSHA1 != string(hash)) ||
|
||||
(ext == extensionSHA256 && pb.HashSHA256 != string(hash)) ||
|
||||
(ext == extensionSHA512 && pb.HashSHA512 != string(hash)) {
|
||||
apiError(ctx, http.StatusBadRequest, "hash mismatch")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
pfci := &packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: params.Filename,
|
||||
},
|
||||
Data: buf,
|
||||
IsLead: false,
|
||||
}
|
||||
|
||||
// If it's the package pom file extract the metadata
|
||||
if ext == ".pom" {
|
||||
pfci.IsLead = true
|
||||
|
||||
var err error
|
||||
pvci.Metadata, err = maven_module.ParsePackageMetaData(buf)
|
||||
if err != nil {
|
||||
log.Error("Error parsing package metadata: %v", err)
|
||||
}
|
||||
|
||||
if pvci.Metadata != nil {
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version)
|
||||
if err != nil && err != packages_model.ErrPackageNotExist {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if pv != nil {
|
||||
raw, err := json.Marshal(pvci.Metadata)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
pv.MetadataJSON = string(raw)
|
||||
if err := packages_model.UpdateVersion(ctx, pv); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
|
||||
pvci,
|
||||
pfci,
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageFile {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
func isChecksumExtension(ext string) bool {
|
||||
return ext == extensionMD5 || ext == extensionSHA1 || ext == extensionSHA256 || ext == extensionSHA512
|
||||
}
|
||||
|
||||
type parameters struct {
|
||||
GroupID string
|
||||
ArtifactID string
|
||||
Version string
|
||||
Filename string
|
||||
IsMeta bool
|
||||
}
|
||||
|
||||
func extractPathParameters(ctx *context.Context) (parameters, error) {
|
||||
parts := strings.Split(ctx.Params("*"), "/")
|
||||
|
||||
p := parameters{
|
||||
Filename: parts[len(parts)-1],
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
73
routers/api/packages/npm/api.go
Normal file
73
routers/api/packages/npm/api.go
Normal file
@@ -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 npm
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sort"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
npm_module "code.gitea.io/gitea/modules/packages/npm"
|
||||
)
|
||||
|
||||
func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *npm_module.PackageMetadata {
|
||||
sort.Slice(pds, func(i, j int) bool {
|
||||
return pds[i].SemVer.LessThan(pds[j].SemVer)
|
||||
})
|
||||
|
||||
versions := make(map[string]*npm_module.PackageMetadataVersion)
|
||||
distTags := make(map[string]string)
|
||||
for _, pd := range pds {
|
||||
versions[pd.SemVer.String()] = createPackageMetadataVersion(registryURL, pd)
|
||||
|
||||
for _, pvp := range pd.Properties {
|
||||
if pvp.Name == npm_module.TagProperty {
|
||||
distTags[pvp.Value] = pd.Version.Version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
latest := pds[len(pds)-1]
|
||||
|
||||
metadata := latest.Metadata.(*npm_module.Metadata)
|
||||
|
||||
return &npm_module.PackageMetadata{
|
||||
ID: latest.Package.Name,
|
||||
Name: latest.Package.Name,
|
||||
DistTags: distTags,
|
||||
Description: metadata.Description,
|
||||
Readme: metadata.Readme,
|
||||
Homepage: metadata.ProjectURL,
|
||||
Author: npm_module.User{Name: metadata.Author},
|
||||
License: metadata.License,
|
||||
Versions: versions,
|
||||
}
|
||||
}
|
||||
|
||||
func createPackageMetadataVersion(registryURL string, pd *packages_model.PackageDescriptor) *npm_module.PackageMetadataVersion {
|
||||
hashBytes, _ := hex.DecodeString(pd.Files[0].Blob.HashSHA512)
|
||||
|
||||
metadata := pd.Metadata.(*npm_module.Metadata)
|
||||
|
||||
return &npm_module.PackageMetadataVersion{
|
||||
ID: fmt.Sprintf("%s@%s", pd.Package.Name, pd.Version.Version),
|
||||
Name: pd.Package.Name,
|
||||
Version: pd.Version.Version,
|
||||
Description: metadata.Description,
|
||||
Author: npm_module.User{Name: metadata.Author},
|
||||
Homepage: metadata.ProjectURL,
|
||||
License: metadata.License,
|
||||
Dependencies: metadata.Dependencies,
|
||||
Readme: metadata.Readme,
|
||||
Dist: npm_module.PackageDistribution{
|
||||
Shasum: pd.Files[0].Blob.HashSHA1,
|
||||
Integrity: "sha512-" + base64.StdEncoding.EncodeToString(hashBytes),
|
||||
Tarball: fmt.Sprintf("%s/%s/-/%s/%s", registryURL, url.QueryEscape(pd.Package.Name), url.PathEscape(pd.Version.Version), url.PathEscape(pd.Files[0].File.LowerName)),
|
||||
},
|
||||
}
|
||||
}
|
||||
293
routers/api/packages/npm/npm.go
Normal file
293
routers/api/packages/npm/npm.go
Normal file
@@ -0,0 +1,293 @@
|
||||
// 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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
npm_module "code.gitea.io/gitea/modules/packages/npm"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
// errInvalidTagName indicates an invalid tag name
|
||||
var errInvalidTagName = errors.New("The tag name is invalid")
|
||||
|
||||
func apiError(ctx *context.Context, status int, obj interface{}) {
|
||||
helper.LogAndProcessError(ctx, status, obj, func(message string) {
|
||||
ctx.JSON(status, map[string]string{
|
||||
"error": message,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// packageNameFromParams gets the package name from the url parameters
|
||||
// Variations: /name/, /@scope/name/, /@scope%2Fname/
|
||||
func packageNameFromParams(ctx *context.Context) string {
|
||||
scope := ctx.Params("scope")
|
||||
id := ctx.Params("id")
|
||||
if scope != "" {
|
||||
return fmt.Sprintf("@%s/%s", scope, id)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// PackageMetadata returns the metadata for a single package
|
||||
func PackageMetadata(ctx *context.Context) {
|
||||
packageName := packageNameFromParams(ctx)
|
||||
|
||||
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(pvs) == 0 {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := createPackageMetadataResponse(
|
||||
setting.AppURL+"api/packages/"+ctx.Package.Owner.Name+"/npm",
|
||||
pds,
|
||||
)
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// DownloadPackageFile serves the content of a package
|
||||
func DownloadPackageFile(ctx *context.Context) {
|
||||
packageName := packageNameFromParams(ctx)
|
||||
packageVersion := ctx.Params("version")
|
||||
filename := ctx.Params("filename")
|
||||
|
||||
s, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
|
||||
ctx,
|
||||
&packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeNpm,
|
||||
Name: packageName,
|
||||
Version: packageVersion,
|
||||
},
|
||||
&packages_service.PackageFileInfo{
|
||||
Filename: filename,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
ctx.ServeStream(s, pf.Name)
|
||||
}
|
||||
|
||||
// UploadPackage creates a new package
|
||||
func UploadPackage(ctx *context.Context) {
|
||||
npmPackage, err := npm_module.ParsePackage(ctx.Req.Body)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(bytes.NewReader(npmPackage.Data), 32*1024*1024)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer buf.Close()
|
||||
|
||||
pv, _, err := packages_service.CreatePackageAndAddFile(
|
||||
&packages_service.PackageCreationInfo{
|
||||
PackageInfo: packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeNpm,
|
||||
Name: npmPackage.Name,
|
||||
Version: npmPackage.Version,
|
||||
},
|
||||
SemverCompatible: true,
|
||||
Creator: ctx.Doer,
|
||||
Metadata: npmPackage.Metadata,
|
||||
},
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: npmPackage.Filename,
|
||||
},
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageVersion {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, tag := range npmPackage.DistTags {
|
||||
if err := setPackageTag(tag, pv, false); err != nil {
|
||||
if err == errInvalidTagName {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
// ListPackageTags returns all tags for a package
|
||||
func ListPackageTags(ctx *context.Context) {
|
||||
packageName := packageNameFromParams(ctx)
|
||||
|
||||
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
tags := make(map[string]string)
|
||||
for _, pv := range pvs {
|
||||
pvps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, npm_module.TagProperty)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
for _, pvp := range pvps {
|
||||
tags[pvp.Value] = pv.Version
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, tags)
|
||||
}
|
||||
|
||||
// AddPackageTag adds a tag to the package
|
||||
func AddPackageTag(ctx *context.Context) {
|
||||
packageName := packageNameFromParams(ctx)
|
||||
|
||||
body, err := io.ReadAll(ctx.Req.Body)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
version := strings.Trim(string(body), "\"") // is as "version" in the body
|
||||
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName, version)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := setPackageTag(ctx.Params("tag"), pv, false); err != nil {
|
||||
if err == errInvalidTagName {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// DeletePackageTag deletes a package tag
|
||||
func DeletePackageTag(ctx *context.Context) {
|
||||
packageName := packageNameFromParams(ctx)
|
||||
|
||||
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(pvs) != 0 {
|
||||
if err := setPackageTag(ctx.Params("tag"), pvs[0], true); err != nil {
|
||||
if err == errInvalidTagName {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setPackageTag(tag string, pv *packages_model.PackageVersion, deleteOnly bool) error {
|
||||
if tag == "" {
|
||||
return errInvalidTagName
|
||||
}
|
||||
_, err := version.NewVersion(tag)
|
||||
if err == nil {
|
||||
return errInvalidTagName
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
PackageID: pv.PackageID,
|
||||
Properties: map[string]string{
|
||||
npm_module.TagProperty: tag,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(pvs) == 1 {
|
||||
pvps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pvs[0].ID, npm_module.TagProperty)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pvp := range pvps {
|
||||
if pvp.Value == tag {
|
||||
if err := packages_model.DeletePropertyByID(ctx, pvp.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !deleteOnly {
|
||||
_, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, npm_module.TagProperty, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
287
routers/api/packages/nuget/api.go
Normal file
287
routers/api/packages/nuget/api.go
Normal file
@@ -0,0 +1,287 @@
|
||||
// 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"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
|
||||
|
||||
"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"},
|
||||
{ID: root + "/symbolpackage", Type: "SymbolPackagePublish/4.9.0"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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"`
|
||||
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, pds []*packages_model.PackageDescriptor) *RegistrationIndexResponse {
|
||||
sort.Slice(pds, func(i, j int) bool {
|
||||
return pds[i].SemVer.LessThan(pds[j].SemVer)
|
||||
})
|
||||
|
||||
items := make([]*RegistrationIndexPageItem, 0, len(pds))
|
||||
for _, p := range pds {
|
||||
items = append(items, createRegistrationIndexPageItem(l, p))
|
||||
}
|
||||
|
||||
return &RegistrationIndexResponse{
|
||||
RegistrationIndexURL: l.GetRegistrationIndexURL(pds[0].Package.Name),
|
||||
Type: []string{"catalog:CatalogRoot", "PackageRegistration", "catalog:Permalink"},
|
||||
Count: 1,
|
||||
Pages: []*RegistrationIndexPage{
|
||||
{
|
||||
RegistrationPageURL: l.GetRegistrationIndexURL(pds[0].Package.Name),
|
||||
Count: len(pds),
|
||||
Lower: normalizeVersion(pds[0].SemVer),
|
||||
Upper: normalizeVersion(pds[len(pds)-1].SemVer),
|
||||
Items: items,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createRegistrationIndexPageItem(l *linkBuilder, pd *packages_model.PackageDescriptor) *RegistrationIndexPageItem {
|
||||
metadata := pd.Metadata.(*nuget_module.Metadata)
|
||||
|
||||
return &RegistrationIndexPageItem{
|
||||
RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
|
||||
PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
|
||||
CatalogEntry: &CatalogEntry{
|
||||
CatalogLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
|
||||
PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
|
||||
ID: pd.Package.Name,
|
||||
Version: pd.Version.Version,
|
||||
Description: metadata.Description,
|
||||
ReleaseNotes: metadata.ReleaseNotes,
|
||||
Authors: metadata.Authors,
|
||||
ProjectURL: metadata.ProjectURL,
|
||||
DependencyGroups: createDependencyGroups(pd),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createDependencyGroups(pd *packages_model.PackageDescriptor) []*PackageDependencyGroup {
|
||||
metadata := pd.Metadata.(*nuget_module.Metadata)
|
||||
|
||||
dependencyGroups := make([]*PackageDependencyGroup, 0, len(metadata.Dependencies))
|
||||
for k, v := range 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, pd *packages_model.PackageDescriptor) *RegistrationLeafResponse {
|
||||
return &RegistrationLeafResponse{
|
||||
Type: []string{"Package", "http://schema.nuget.org/catalog#Permalink"},
|
||||
Listed: true,
|
||||
Published: time.Unix(int64(pd.Version.CreatedUnix), 0),
|
||||
RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
|
||||
PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
|
||||
RegistrationIndexURL: l.GetRegistrationIndexURL(pd.Package.Name),
|
||||
}
|
||||
}
|
||||
|
||||
// PackageVersionsResponse https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#response
|
||||
type PackageVersionsResponse struct {
|
||||
Versions []string `json:"versions"`
|
||||
}
|
||||
|
||||
func createPackageVersionsResponse(pds []*packages_model.PackageDescriptor) *PackageVersionsResponse {
|
||||
versions := make([]string, 0, len(pds))
|
||||
for _, pd := range pds {
|
||||
versions = append(versions, normalizeVersion(pd.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"`
|
||||
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, pds []*packages_model.PackageDescriptor) *SearchResultResponse {
|
||||
data := make([]*SearchResult, 0, len(pds))
|
||||
|
||||
if len(pds) > 0 {
|
||||
groupID := pds[0].Package.Name
|
||||
group := make([]*packages_model.PackageDescriptor, 0, 10)
|
||||
|
||||
for i := 0; i < len(pds); i++ {
|
||||
if groupID != pds[i].Package.Name {
|
||||
data = append(data, createSearchResult(l, group))
|
||||
groupID = pds[i].Package.Name
|
||||
group = group[:0]
|
||||
}
|
||||
group = append(group, pds[i])
|
||||
}
|
||||
data = append(data, createSearchResult(l, group))
|
||||
}
|
||||
|
||||
return &SearchResultResponse{
|
||||
TotalHits: totalHits,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func createSearchResult(l *linkBuilder, pds []*packages_model.PackageDescriptor) *SearchResult {
|
||||
latest := pds[0]
|
||||
versions := make([]*SearchResultVersion, 0, len(pds))
|
||||
for _, pd := range pds {
|
||||
if latest.SemVer.LessThan(pd.SemVer) {
|
||||
latest = pd
|
||||
}
|
||||
|
||||
versions = append(versions, &SearchResultVersion{
|
||||
RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
|
||||
Version: pd.Version.Version,
|
||||
})
|
||||
}
|
||||
|
||||
metadata := latest.Metadata.(*nuget_module.Metadata)
|
||||
|
||||
return &SearchResult{
|
||||
ID: latest.Package.Name,
|
||||
Version: latest.Version.Version,
|
||||
Versions: versions,
|
||||
Description: metadata.Description,
|
||||
Authors: metadata.Authors,
|
||||
ProjectURL: metadata.ProjectURL,
|
||||
RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Package.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()
|
||||
}
|
||||
28
routers/api/packages/nuget/links.go
Normal file
28
routers/api/packages/nuget/links.go
Normal file
@@ -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)
|
||||
}
|
||||
|
||||
// GetRegistrationLeafURL 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)
|
||||
}
|
||||
|
||||
// GetPackageDownloadURL 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)
|
||||
}
|
||||
415
routers/api/packages/nuget/nuget.go
Normal file
415
routers/api/packages/nuget/nuget.go
Normal file
@@ -0,0 +1,415 @@
|
||||
// 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"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
func apiError(ctx *context.Context, status int, obj interface{}) {
|
||||
helper.LogAndProcessError(ctx, status, obj, func(message string) {
|
||||
ctx.JSON(status, map[string]string{
|
||||
"Message": message,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ServiceIndex https://docs.microsoft.com/en-us/nuget/api/service-index
|
||||
func ServiceIndex(ctx *context.Context) {
|
||||
resp := createServiceIndexResponse(setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget")
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// SearchService https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages
|
||||
func SearchService(ctx *context.Context) {
|
||||
pvs, count, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeNuGet,
|
||||
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
|
||||
Paginator: db.NewAbsoluteListOptions(
|
||||
ctx.FormInt("skip"),
|
||||
ctx.FormInt("take"),
|
||||
),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := createSearchResultResponse(
|
||||
&linkBuilder{setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"},
|
||||
count,
|
||||
pds,
|
||||
)
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// RegistrationIndex https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-index
|
||||
func RegistrationIndex(ctx *context.Context) {
|
||||
packageName := ctx.Params("id")
|
||||
|
||||
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(pvs) == 0 {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := createRegistrationIndexResponse(
|
||||
&linkBuilder{setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"},
|
||||
pds,
|
||||
)
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// RegistrationLeaf https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf
|
||||
func RegistrationLeaf(ctx *context.Context) {
|
||||
packageName := ctx.Params("id")
|
||||
packageVersion := strings.TrimSuffix(ctx.Params("version"), ".json")
|
||||
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(db.DefaultContext, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
pd, err := packages_model.GetPackageDescriptor(ctx, pv)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := createRegistrationLeafResponse(
|
||||
&linkBuilder{setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"},
|
||||
pd,
|
||||
)
|
||||
|
||||
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.Context) {
|
||||
packageName := ctx.Params("id")
|
||||
|
||||
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(pvs) == 0 {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := createPackageVersionsResponse(pds)
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// DownloadPackageFile https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg
|
||||
func DownloadPackageFile(ctx *context.Context) {
|
||||
packageName := ctx.Params("id")
|
||||
packageVersion := ctx.Params("version")
|
||||
filename := ctx.Params("filename")
|
||||
|
||||
s, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
|
||||
ctx,
|
||||
&packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeNuGet,
|
||||
Name: packageName,
|
||||
Version: packageVersion,
|
||||
},
|
||||
&packages_service.PackageFileInfo{
|
||||
Filename: filename,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
ctx.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.Context) {
|
||||
np, buf, closables := processUploadedFile(ctx, nuget_module.DependencyPackage)
|
||||
defer func() {
|
||||
for _, c := range closables {
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
if np == nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, _, err := packages_service.CreatePackageAndAddFile(
|
||||
&packages_service.PackageCreationInfo{
|
||||
PackageInfo: packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeNuGet,
|
||||
Name: np.ID,
|
||||
Version: np.Version,
|
||||
},
|
||||
SemverCompatible: true,
|
||||
Creator: ctx.Doer,
|
||||
Metadata: np.Metadata,
|
||||
},
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: strings.ToLower(fmt.Sprintf("%s.%s.nupkg", np.ID, np.Version)),
|
||||
},
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageVersion {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
// 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.Context) {
|
||||
np, buf, closables := processUploadedFile(ctx, nuget_module.SymbolsPackage)
|
||||
defer func() {
|
||||
for _, c := range closables {
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
if np == nil {
|
||||
return
|
||||
}
|
||||
|
||||
pdbs, err := nuget_module.ExtractPortablePdb(buf, buf.Size())
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
defer pdbs.Close()
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
pi := &packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeNuGet,
|
||||
Name: np.ID,
|
||||
Version: np.Version,
|
||||
}
|
||||
|
||||
_, _, err = packages_service.AddFileToExistingPackage(
|
||||
pi,
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: strings.ToLower(fmt.Sprintf("%s.%s.snupkg", np.ID, np.Version)),
|
||||
},
|
||||
Data: buf,
|
||||
IsLead: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case packages_model.ErrPackageNotExist:
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
case packages_model.ErrDuplicatePackageFile:
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, pdb := range pdbs {
|
||||
_, _, err := packages_service.AddFileToExistingPackage(
|
||||
pi,
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: strings.ToLower(pdb.Name),
|
||||
CompositeKey: strings.ToLower(pdb.ID),
|
||||
},
|
||||
Data: pdb.Content,
|
||||
IsLead: false,
|
||||
Properties: map[string]string{
|
||||
nuget_module.PropertySymbolID: strings.ToLower(pdb.ID),
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageFile:
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
func processUploadedFile(ctx *context.Context, expectedType nuget_module.PackageType) (*nuget_module.Package, *packages_module.HashedBuffer, []io.Closer) {
|
||||
closables := make([]io.Closer, 0, 2)
|
||||
|
||||
upload, close, err := ctx.UploadStream()
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return nil, nil, closables
|
||||
}
|
||||
|
||||
if close {
|
||||
closables = append(closables, upload)
|
||||
}
|
||||
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(upload, 32*1024*1024)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return nil, nil, closables
|
||||
}
|
||||
closables = append(closables, buf)
|
||||
|
||||
np, err := nuget_module.ParsePackageMetaData(buf, buf.Size())
|
||||
if err != nil {
|
||||
if err == nuget_module.ErrMissingNuspecFile || err == nuget_module.ErrNuspecFileTooLarge || err == nuget_module.ErrNuspecInvalidID || err == nuget_module.ErrNuspecInvalidVersion {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return nil, nil, closables
|
||||
}
|
||||
if np.PackageType != expectedType {
|
||||
apiError(ctx, http.StatusBadRequest, errors.New("unexpected package type"))
|
||||
return nil, nil, closables
|
||||
}
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return nil, nil, closables
|
||||
}
|
||||
return np, buf, closables
|
||||
}
|
||||
|
||||
// DownloadSymbolFile https://github.com/dotnet/symstore/blob/main/docs/specs/Simple_Symbol_Query_Protocol.md#request
|
||||
func DownloadSymbolFile(ctx *context.Context) {
|
||||
filename := ctx.Params("filename")
|
||||
guid := ctx.Params("guid")
|
||||
filename2 := ctx.Params("filename2")
|
||||
|
||||
if filename != filename2 {
|
||||
apiError(ctx, http.StatusBadRequest, nil)
|
||||
return
|
||||
}
|
||||
|
||||
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
PackageType: string(packages_model.TypeNuGet),
|
||||
Query: filename,
|
||||
Properties: map[string]string{
|
||||
nuget_module.PropertySymbolID: strings.ToLower(guid),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(pfs) != 1 {
|
||||
apiError(ctx, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
s, _, err := packages_service.GetPackageFileStream(ctx, pfs[0])
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
ctx.ServeStream(s, pfs[0].Name)
|
||||
}
|
||||
|
||||
// DeletePackage hard deletes the package
|
||||
// https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#delete-a-package
|
||||
func DeletePackage(ctx *context.Context) {
|
||||
packageName := ctx.Params("id")
|
||||
packageVersion := ctx.Params("version")
|
||||
|
||||
err := packages_service.RemovePackageVersionByNameAndVersion(
|
||||
ctx.Doer,
|
||||
&packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeNuGet,
|
||||
Name: packageName,
|
||||
Version: packageVersion,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
174
routers/api/packages/pypi/pypi.go
Normal file
174
routers/api/packages/pypi/pypi.go
Normal file
@@ -0,0 +1,174 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
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"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
packages_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]*))?$`)
|
||||
|
||||
func apiError(ctx *context.Context, status int, obj interface{}) {
|
||||
helper.LogAndProcessError(ctx, status, obj, func(message string) {
|
||||
ctx.PlainText(status, message)
|
||||
})
|
||||
}
|
||||
|
||||
// PackageMetadata returns the metadata for a single package
|
||||
func PackageMetadata(ctx *context.Context) {
|
||||
packageName := normalizer.Replace(ctx.Params("id"))
|
||||
|
||||
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypePyPI, packageName)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(pvs) == 0 {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["RegistryURL"] = setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/pypi"
|
||||
ctx.Data["PackageDescriptor"] = pds[0]
|
||||
ctx.Data["PackageDescriptors"] = pds
|
||||
ctx.Render = templates.HTMLRenderer()
|
||||
ctx.HTML(http.StatusOK, "api/packages/pypi/simple")
|
||||
}
|
||||
|
||||
// DownloadPackageFile serves the content of a package
|
||||
func DownloadPackageFile(ctx *context.Context) {
|
||||
packageName := normalizer.Replace(ctx.Params("id"))
|
||||
packageVersion := ctx.Params("version")
|
||||
filename := ctx.Params("filename")
|
||||
|
||||
s, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
|
||||
ctx,
|
||||
&packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypePyPI,
|
||||
Name: packageName,
|
||||
Version: packageVersion,
|
||||
},
|
||||
&packages_service.PackageFileInfo{
|
||||
Filename: filename,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
ctx.ServeStream(s, pf.Name)
|
||||
}
|
||||
|
||||
// UploadPackageFile adds a file to the package. If the package does not exist, it gets created.
|
||||
func UploadPackageFile(ctx *context.Context) {
|
||||
file, fileHeader, err := ctx.Req.FormFile("content")
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(file, 32*1024*1024)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer buf.Close()
|
||||
|
||||
_, _, hashSHA256, _ := buf.Sums()
|
||||
|
||||
if !strings.EqualFold(ctx.Req.FormValue("sha256_digest"), fmt.Sprintf("%x", hashSHA256)) {
|
||||
apiError(ctx, http.StatusBadRequest, "hash mismatch")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
packageName := normalizer.Replace(ctx.Req.FormValue("name"))
|
||||
packageVersion := ctx.Req.FormValue("version")
|
||||
if !nameMatcher.MatchString(packageName) || !versionMatcher.MatchString(packageVersion) {
|
||||
apiError(ctx, http.StatusBadRequest, "invalid name or version")
|
||||
return
|
||||
}
|
||||
|
||||
projectURL := ctx.Req.FormValue("home_page")
|
||||
if !validation.IsValidURL(projectURL) {
|
||||
projectURL = ""
|
||||
}
|
||||
|
||||
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
|
||||
&packages_service.PackageCreationInfo{
|
||||
PackageInfo: packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypePyPI,
|
||||
Name: packageName,
|
||||
Version: packageVersion,
|
||||
},
|
||||
SemverCompatible: true,
|
||||
Creator: ctx.Doer,
|
||||
Metadata: &pypi_module.Metadata{
|
||||
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"),
|
||||
},
|
||||
},
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: fileHeader.Filename,
|
||||
},
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageFile {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
294
routers/api/packages/rubygems/rubygems.go
Normal file
294
routers/api/packages/rubygems/rubygems.go
Normal file
@@ -0,0 +1,294 @@
|
||||
// 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"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
rubygems_module "code.gitea.io/gitea/modules/packages/rubygems"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
func apiError(ctx *context.Context, status int, obj interface{}) {
|
||||
helper.LogAndProcessError(ctx, status, obj, func(message string) {
|
||||
ctx.PlainText(status, message)
|
||||
})
|
||||
}
|
||||
|
||||
// EnumeratePackages serves the package list
|
||||
func EnumeratePackages(ctx *context.Context) {
|
||||
packages, err := packages_model.GetVersionsByPackageType(ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems)
|
||||
if err != nil {
|
||||
apiError(ctx, 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.Context) {
|
||||
pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeRubyGems,
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
enumeratePackages(ctx, "latest_specs.4.8", pvs)
|
||||
}
|
||||
|
||||
// EnumeratePackagesPreRelease is not supported and serves an empty list
|
||||
func EnumeratePackagesPreRelease(ctx *context.Context) {
|
||||
enumeratePackages(ctx, "prerelease_specs.4.8", []*packages_model.PackageVersion{})
|
||||
}
|
||||
|
||||
func enumeratePackages(ctx *context.Context, filename string, pvs []*packages_model.PackageVersion) {
|
||||
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
specs := make([]interface{}, 0, len(pds))
|
||||
for _, p := range pds {
|
||||
specs = append(specs, []interface{}{
|
||||
p.Package.Name,
|
||||
&rubygems_module.RubyUserMarshal{
|
||||
Name: "Gem::Version",
|
||||
Value: []string{p.Version.Version},
|
||||
},
|
||||
p.Metadata.(*rubygems_module.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.Context) {
|
||||
filename := ctx.Params("filename")
|
||||
|
||||
if !strings.HasSuffix(filename, ".gemspec.rz") {
|
||||
apiError(ctx, http.StatusNotImplemented, nil)
|
||||
return
|
||||
}
|
||||
|
||||
pvs, err := getVersionsByFilename(ctx, filename[:len(filename)-10]+"gem")
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(pvs) != 1 {
|
||||
apiError(ctx, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
pd, err := packages_model.GetPackageDescriptor(ctx, pvs[0])
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetServeHeaders(filename)
|
||||
|
||||
zw := zlib.NewWriter(ctx.Resp)
|
||||
defer zw.Close()
|
||||
|
||||
metadata := pd.Metadata.(*rubygems_module.Metadata)
|
||||
|
||||
// create a Ruby Gem::Specification object
|
||||
spec := &rubygems_module.RubyUserDef{
|
||||
Name: "Gem::Specification",
|
||||
Value: []interface{}{
|
||||
"3.2.3", // @rubygems_version
|
||||
4, // @specification_version,
|
||||
pd.Package.Name,
|
||||
&rubygems_module.RubyUserMarshal{
|
||||
Name: "Gem::Version",
|
||||
Value: []string{pd.Version.Version},
|
||||
},
|
||||
nil, // date
|
||||
metadata.Summary, // @summary
|
||||
nil, // @required_ruby_version
|
||||
nil, // @required_rubygems_version
|
||||
metadata.Platform, // @original_platform
|
||||
[]interface{}{}, // @dependencies
|
||||
nil, // rubyforge_project
|
||||
"", // @email
|
||||
metadata.Authors,
|
||||
metadata.Description,
|
||||
metadata.ProjectURL,
|
||||
true, // has_rdoc
|
||||
metadata.Platform, // @new_platform
|
||||
nil,
|
||||
metadata.Licenses,
|
||||
},
|
||||
}
|
||||
|
||||
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.Context) {
|
||||
filename := ctx.Params("filename")
|
||||
|
||||
pvs, err := getVersionsByFilename(ctx, filename)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(pvs) != 1 {
|
||||
apiError(ctx, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
s, pf, err := packages_service.GetFileStreamByPackageVersion(
|
||||
ctx,
|
||||
pvs[0],
|
||||
&packages_service.PackageFileInfo{
|
||||
Filename: filename,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageFileNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, 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.Context) {
|
||||
upload, close, err := ctx.UploadStream()
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
if close {
|
||||
defer upload.Close()
|
||||
}
|
||||
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(upload, 32*1024*1024)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer buf.Close()
|
||||
|
||||
rp, err := rubygems_module.ParsePackageMetaData(buf)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
var filename string
|
||||
if rp.Metadata.Platform == "" || rp.Metadata.Platform == "ruby" {
|
||||
filename = strings.ToLower(fmt.Sprintf("%s-%s.gem", rp.Name, rp.Version))
|
||||
} else {
|
||||
filename = strings.ToLower(fmt.Sprintf("%s-%s-%s.gem", rp.Name, rp.Version, rp.Metadata.Platform))
|
||||
}
|
||||
|
||||
_, _, err = packages_service.CreatePackageAndAddFile(
|
||||
&packages_service.PackageCreationInfo{
|
||||
PackageInfo: packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeRubyGems,
|
||||
Name: rp.Name,
|
||||
Version: rp.Version,
|
||||
},
|
||||
SemverCompatible: true,
|
||||
Creator: ctx.Doer,
|
||||
Metadata: rp.Metadata,
|
||||
},
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: filename,
|
||||
},
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageVersion {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
// DeletePackage deletes a package
|
||||
func DeletePackage(ctx *context.Context) {
|
||||
// Go populates the form only for POST, PUT and PATCH requests
|
||||
if err := ctx.Req.ParseMultipartForm(32 << 20); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
packageName := ctx.FormString("gem_name")
|
||||
packageVersion := ctx.FormString("version")
|
||||
|
||||
err := packages_service.RemovePackageVersionByNameAndVersion(
|
||||
ctx.Doer,
|
||||
&packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeRubyGems,
|
||||
Name: packageName,
|
||||
Version: packageVersion,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
|
||||
func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_model.PackageVersion, error) {
|
||||
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeRubyGems,
|
||||
HasFileWithName: filename,
|
||||
})
|
||||
return pvs, err
|
||||
}
|
||||
@@ -110,7 +110,7 @@ func AdoptRepository(ctx *context.APIContext) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
if _, err := repo_service.AdoptRepository(ctx.User, ctxUser, models.CreateRepoOptions{
|
||||
if _, err := repo_service.AdoptRepository(ctx.Doer, ctxUser, models.CreateRepoOptions{
|
||||
Name: repoName,
|
||||
IsPrivate: true,
|
||||
}); err != nil {
|
||||
@@ -173,7 +173,7 @@ func DeleteUnadoptedRepository(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo_service.DeleteUnadoptedRepository(ctx.User, ctxUser, repoName); err != nil {
|
||||
if err := repo_service.DeleteUnadoptedRepository(ctx.Doer, ctxUser, repoName); err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func PostCronTask(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
task.Run()
|
||||
log.Trace("Cron Task %s started by admin(%s)", task.Name, ctx.User.Name)
|
||||
log.Trace("Cron Task %s started by admin(%s)", task.Name, ctx.Doer.Name)
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -8,14 +8,13 @@ package admin
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/user"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
)
|
||||
|
||||
@@ -45,18 +44,15 @@ func CreateOrg(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateOrgOption)
|
||||
u := user.GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
visibility := api.VisibleTypePublic
|
||||
if form.Visibility != "" {
|
||||
visibility = api.VisibilityModes[form.Visibility]
|
||||
}
|
||||
|
||||
org := &models.Organization{
|
||||
org := &organization.Organization{
|
||||
Name: form.UserName,
|
||||
FullName: form.FullName,
|
||||
Description: form.Description,
|
||||
@@ -67,7 +63,7 @@ func CreateOrg(ctx *context.APIContext) {
|
||||
Visibility: visibility,
|
||||
}
|
||||
|
||||
if err := models.CreateOrganization(org, u); err != nil {
|
||||
if err := organization.CreateOrganization(org, ctx.ContextUser); err != nil {
|
||||
if user_model.IsErrUserAlreadyExist(err) ||
|
||||
db.IsErrNameReserved(err) ||
|
||||
db.IsErrNameCharsNotAllowed(err) ||
|
||||
@@ -107,7 +103,7 @@ func GetAllOrgs(ctx *context.APIContext) {
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
users, maxResults, err := user_model.SearchUsers(&user_model.SearchUserOptions{
|
||||
Actor: ctx.User,
|
||||
Actor: ctx.Doer,
|
||||
Type: user_model.UserTypeOrganization,
|
||||
OrderBy: db.SearchOrderByAlphabetically,
|
||||
ListOptions: listOptions,
|
||||
@@ -119,7 +115,7 @@ func GetAllOrgs(ctx *context.APIContext) {
|
||||
}
|
||||
orgs := make([]*api.Organization, len(users))
|
||||
for i := range users {
|
||||
orgs[i] = convert.ToOrganization(models.OrgFromUser(users[i]))
|
||||
orgs[i] = convert.ToOrganization(organization.OrgFromUser(users[i]))
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/repo"
|
||||
"code.gitea.io/gitea/routers/api/v1/user"
|
||||
)
|
||||
|
||||
// CreateRepo api for creating a repository
|
||||
@@ -42,11 +41,8 @@ func CreateRepo(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/error"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
form := web.GetForm(ctx).(*api.CreateRepoOption)
|
||||
owner := user.GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
repo.CreateUserRepo(ctx, owner, *form)
|
||||
form := web.GetForm(ctx).(*api.CreateRepoOption)
|
||||
|
||||
repo.CreateUserRepo(ctx, ctx.ContextUser, *form)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/password"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/user"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
@@ -73,6 +74,7 @@ func CreateUser(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateUserOption)
|
||||
|
||||
u := &user_model.User{
|
||||
@@ -81,7 +83,6 @@ func CreateUser(ctx *context.APIContext) {
|
||||
Email: form.Email,
|
||||
Passwd: form.Password,
|
||||
MustChangePassword: true,
|
||||
IsActive: true,
|
||||
LoginType: auth.Plain,
|
||||
}
|
||||
if form.MustChangePassword != nil {
|
||||
@@ -107,11 +108,17 @@ func CreateUser(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
var overwriteDefault *user_model.CreateUserOverwriteOptions
|
||||
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
||||
IsActive: util.OptionalBoolTrue,
|
||||
}
|
||||
|
||||
if form.Restricted != nil {
|
||||
overwriteDefault.IsRestricted = util.OptionalBoolOf(*form.Restricted)
|
||||
}
|
||||
|
||||
if form.Visibility != "" {
|
||||
overwriteDefault = &user_model.CreateUserOverwriteOptions{
|
||||
Visibility: api.VisibilityModes[form.Visibility],
|
||||
}
|
||||
visibility := api.VisibilityModes[form.Visibility]
|
||||
overwriteDefault.Visibility = &visibility
|
||||
}
|
||||
|
||||
if err := user_model.CreateUser(u, overwriteDefault); err != nil {
|
||||
@@ -119,6 +126,7 @@ func CreateUser(ctx *context.APIContext) {
|
||||
user_model.IsErrEmailAlreadyUsed(err) ||
|
||||
db.IsErrNameReserved(err) ||
|
||||
db.IsErrNameCharsNotAllowed(err) ||
|
||||
user_model.IsErrEmailCharIsNotSupported(err) ||
|
||||
user_model.IsErrEmailInvalid(err) ||
|
||||
db.IsErrNamePatternNotAllowed(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||
@@ -127,13 +135,13 @@ func CreateUser(ctx *context.APIContext) {
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Trace("Account created by admin (%s): %s", ctx.User.Name, u.Name)
|
||||
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
|
||||
|
||||
// Send email notification.
|
||||
if form.SendNotify {
|
||||
mailer.SendRegisterNotifyMail(u)
|
||||
}
|
||||
ctx.JSON(http.StatusCreated, convert.ToUser(u, ctx.User))
|
||||
ctx.JSON(http.StatusCreated, convert.ToUser(u, ctx.Doer))
|
||||
}
|
||||
|
||||
// EditUser api for modifying a user's information
|
||||
@@ -162,13 +170,10 @@ func EditUser(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
form := web.GetForm(ctx).(*api.EditUserOption)
|
||||
u := user.GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
parseAuthSource(ctx, u, form.SourceID, form.LoginName)
|
||||
form := web.GetForm(ctx).(*api.EditUserOption)
|
||||
|
||||
parseAuthSource(ctx, ctx.ContextUser, form.SourceID, form.LoginName)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -192,24 +197,24 @@ func EditUser(ctx *context.APIContext) {
|
||||
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
|
||||
return
|
||||
}
|
||||
if u.Salt, err = user_model.GetUserSalt(); err != nil {
|
||||
if ctx.ContextUser.Salt, err = user_model.GetUserSalt(); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
|
||||
return
|
||||
}
|
||||
if err = u.SetPassword(form.Password); err != nil {
|
||||
if err = ctx.ContextUser.SetPassword(form.Password); err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if form.MustChangePassword != nil {
|
||||
u.MustChangePassword = *form.MustChangePassword
|
||||
ctx.ContextUser.MustChangePassword = *form.MustChangePassword
|
||||
}
|
||||
|
||||
u.LoginName = form.LoginName
|
||||
ctx.ContextUser.LoginName = form.LoginName
|
||||
|
||||
if form.FullName != nil {
|
||||
u.FullName = *form.FullName
|
||||
ctx.ContextUser.FullName = *form.FullName
|
||||
}
|
||||
var emailChanged bool
|
||||
if form.Email != nil {
|
||||
@@ -224,57 +229,59 @@ func EditUser(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
emailChanged = !strings.EqualFold(u.Email, email)
|
||||
u.Email = email
|
||||
emailChanged = !strings.EqualFold(ctx.ContextUser.Email, email)
|
||||
ctx.ContextUser.Email = email
|
||||
}
|
||||
if form.Website != nil {
|
||||
u.Website = *form.Website
|
||||
ctx.ContextUser.Website = *form.Website
|
||||
}
|
||||
if form.Location != nil {
|
||||
u.Location = *form.Location
|
||||
ctx.ContextUser.Location = *form.Location
|
||||
}
|
||||
if form.Description != nil {
|
||||
u.Description = *form.Description
|
||||
ctx.ContextUser.Description = *form.Description
|
||||
}
|
||||
if form.Active != nil {
|
||||
u.IsActive = *form.Active
|
||||
ctx.ContextUser.IsActive = *form.Active
|
||||
}
|
||||
if len(form.Visibility) != 0 {
|
||||
u.Visibility = api.VisibilityModes[form.Visibility]
|
||||
ctx.ContextUser.Visibility = api.VisibilityModes[form.Visibility]
|
||||
}
|
||||
if form.Admin != nil {
|
||||
u.IsAdmin = *form.Admin
|
||||
ctx.ContextUser.IsAdmin = *form.Admin
|
||||
}
|
||||
if form.AllowGitHook != nil {
|
||||
u.AllowGitHook = *form.AllowGitHook
|
||||
ctx.ContextUser.AllowGitHook = *form.AllowGitHook
|
||||
}
|
||||
if form.AllowImportLocal != nil {
|
||||
u.AllowImportLocal = *form.AllowImportLocal
|
||||
ctx.ContextUser.AllowImportLocal = *form.AllowImportLocal
|
||||
}
|
||||
if form.MaxRepoCreation != nil {
|
||||
u.MaxRepoCreation = *form.MaxRepoCreation
|
||||
ctx.ContextUser.MaxRepoCreation = *form.MaxRepoCreation
|
||||
}
|
||||
if form.AllowCreateOrganization != nil {
|
||||
u.AllowCreateOrganization = *form.AllowCreateOrganization
|
||||
ctx.ContextUser.AllowCreateOrganization = *form.AllowCreateOrganization
|
||||
}
|
||||
if form.ProhibitLogin != nil {
|
||||
u.ProhibitLogin = *form.ProhibitLogin
|
||||
ctx.ContextUser.ProhibitLogin = *form.ProhibitLogin
|
||||
}
|
||||
if form.Restricted != nil {
|
||||
u.IsRestricted = *form.Restricted
|
||||
ctx.ContextUser.IsRestricted = *form.Restricted
|
||||
}
|
||||
|
||||
if err := user_model.UpdateUser(u, emailChanged); err != nil {
|
||||
if user_model.IsErrEmailAlreadyUsed(err) || user_model.IsErrEmailInvalid(err) {
|
||||
if err := user_model.UpdateUser(ctx.ContextUser, emailChanged); err != nil {
|
||||
if user_model.IsErrEmailAlreadyUsed(err) ||
|
||||
user_model.IsErrEmailCharIsNotSupported(err) ||
|
||||
user_model.IsErrEmailInvalid(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Trace("Account profile updated by admin (%s): %s", ctx.User.Name, u.Name)
|
||||
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToUser(u, ctx.User))
|
||||
ctx.JSON(http.StatusOK, convert.ToUser(ctx.ContextUser, ctx.Doer))
|
||||
}
|
||||
|
||||
// DeleteUser api for deleting a user
|
||||
@@ -298,26 +305,28 @@ func DeleteUser(ctx *context.APIContext) {
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
u := user.GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
if ctx.ContextUser.IsOrganization() {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
|
||||
return
|
||||
}
|
||||
|
||||
if u.IsOrganization() {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", u.Name))
|
||||
// admin should not delete themself
|
||||
if ctx.ContextUser.ID == ctx.Doer.ID {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("you cannot delete yourself"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_service.DeleteUser(u); err != nil {
|
||||
if err := user_service.DeleteUser(ctx.ContextUser); err != nil {
|
||||
if models.IsErrUserOwnRepos(err) ||
|
||||
models.IsErrUserHasOrgs(err) {
|
||||
models.IsErrUserHasOrgs(err) ||
|
||||
models.IsErrUserOwnPackages(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Trace("Account deleted by admin(%s): %s", ctx.User.Name, u.Name)
|
||||
log.Trace("Account deleted by admin(%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -348,12 +357,10 @@ func CreatePublicKey(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateKeyOption)
|
||||
u := user.GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
user.CreateUserPublicKey(ctx, *form, u.ID)
|
||||
|
||||
user.CreateUserPublicKey(ctx, *form, ctx.ContextUser.ID)
|
||||
}
|
||||
|
||||
// DeleteUserPublicKey api for deleting a user's public key
|
||||
@@ -383,12 +390,7 @@ func DeleteUserPublicKey(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
u := user.GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := asymkey_service.DeletePublicKey(u, ctx.ParamsInt64(":id")); err != nil {
|
||||
if err := asymkey_service.DeletePublicKey(ctx.ContextUser, ctx.ParamsInt64(":id")); err != nil {
|
||||
if asymkey_model.IsErrKeyNotExist(err) {
|
||||
ctx.NotFound()
|
||||
} else if asymkey_model.IsErrKeyAccessDenied(err) {
|
||||
@@ -398,7 +400,7 @@ func DeleteUserPublicKey(ctx *context.APIContext) {
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Trace("Key deleted by admin(%s): %s", ctx.User.Name, u.Name)
|
||||
log.Trace("Key deleted by admin(%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -428,7 +430,7 @@ func GetAllUsers(ctx *context.APIContext) {
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
users, maxResults, err := user_model.SearchUsers(&user_model.SearchUserOptions{
|
||||
Actor: ctx.User,
|
||||
Actor: ctx.Doer,
|
||||
Type: user_model.UserTypeIndividual,
|
||||
OrderBy: db.SearchOrderByAlphabetically,
|
||||
ListOptions: listOptions,
|
||||
@@ -440,7 +442,7 @@ func GetAllUsers(ctx *context.APIContext) {
|
||||
|
||||
results := make([]*api.User, len(users))
|
||||
for i := range users {
|
||||
results[i] = convert.ToUser(users[i], ctx.User)
|
||||
results[i] = convert.ToUser(users[i], ctx.Doer)
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
|
||||
|
||||
@@ -71,6 +71,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@@ -84,10 +86,12 @@ 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"
|
||||
"code.gitea.io/gitea/routers/api/v1/repo"
|
||||
"code.gitea.io/gitea/routers/api/v1/settings"
|
||||
"code.gitea.io/gitea/routers/api/v1/user"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
context_service "code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
|
||||
_ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation
|
||||
@@ -104,7 +108,7 @@ func sudo() func(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if len(sudo) > 0 {
|
||||
if ctx.IsSigned && ctx.User.IsAdmin {
|
||||
if ctx.IsSigned && ctx.Doer.IsAdmin {
|
||||
user, err := user_model.GetUserByName(sudo)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
@@ -114,8 +118,8 @@ func sudo() func(ctx *context.APIContext) {
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Trace("Sudo from (%s) to: %s", ctx.User.Name, user.Name)
|
||||
ctx.User = user
|
||||
log.Trace("Sudo from (%s) to: %s", ctx.Doer.Name, user.Name)
|
||||
ctx.Doer = user
|
||||
} else {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "Only administrators allowed to sudo.",
|
||||
@@ -137,8 +141,8 @@ func repoAssignment() func(ctx *context.APIContext) {
|
||||
)
|
||||
|
||||
// Check if the user is the same as the repository owner.
|
||||
if ctx.IsSigned && ctx.User.LowerName == strings.ToLower(userName) {
|
||||
owner = ctx.User
|
||||
if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(userName) {
|
||||
owner = ctx.Doer
|
||||
} else {
|
||||
owner, err = user_model.GetUserByName(userName)
|
||||
if err != nil {
|
||||
@@ -157,6 +161,7 @@ func repoAssignment() func(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
ctx.Repo.Owner = owner
|
||||
ctx.ContextUser = owner
|
||||
|
||||
// Get repository.
|
||||
repo, err := repo_model.GetRepositoryByName(owner.ID, repoName)
|
||||
@@ -179,7 +184,7 @@ func repoAssignment() func(ctx *context.APIContext) {
|
||||
repo.Owner = owner
|
||||
ctx.Repo.Repository = repo
|
||||
|
||||
ctx.Repo.Permission, err = models.GetUserRepoPermission(repo, ctx.User)
|
||||
ctx.Repo.Permission, err = models.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
|
||||
return
|
||||
@@ -192,6 +197,15 @@ func repoAssignment() func(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
|
||||
ctx.Error(http.StatusForbidden, "reqPackageAccess", "user should have specific permission or be a site admin")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Contexter middleware already checks token for user sign in process.
|
||||
func reqToken() func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
@@ -203,7 +217,6 @@ func reqToken() func(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
if ctx.IsSigned {
|
||||
ctx.RequireCSRF()
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusUnauthorized, "reqToken", "token is required")
|
||||
@@ -271,6 +284,15 @@ func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
// reqRepoBranchWriter user should have a permission to write to a branch, or be a site admin
|
||||
func reqRepoBranchWriter(ctx *context.APIContext) {
|
||||
options, ok := web.GetForm(ctx).(api.FileOptionInterface)
|
||||
if !ok || (!ctx.Repo.CanWriteToBranch(ctx.Doer, options.Branch()) && !ctx.IsUserSiteAdmin()) {
|
||||
ctx.Error(http.StatusForbidden, "reqRepoBranchWriter", "user should have a permission to write to this branch")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// reqRepoReader user should have specific read permission or be a repo admin or a site admin
|
||||
func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
@@ -308,7 +330,7 @@ func reqOrgOwnership() func(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
isOwner, err := models.IsOrganizationOwner(orgID, ctx.User.ID)
|
||||
isOwner, err := organization.IsOrganizationOwner(ctx, orgID, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOrganizationOwner", err)
|
||||
return
|
||||
@@ -335,7 +357,7 @@ func reqTeamMembership() func(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
orgID := ctx.Org.Team.OrgID
|
||||
isOwner, err := models.IsOrganizationOwner(orgID, ctx.User.ID)
|
||||
isOwner, err := organization.IsOrganizationOwner(ctx, orgID, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOrganizationOwner", err)
|
||||
return
|
||||
@@ -343,11 +365,11 @@ func reqTeamMembership() func(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if isTeamMember, err := models.IsTeamMember(orgID, ctx.Org.Team.ID, ctx.User.ID); err != nil {
|
||||
if isTeamMember, err := organization.IsTeamMember(ctx, orgID, ctx.Org.Team.ID, ctx.Doer.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsTeamMember", err)
|
||||
return
|
||||
} else if !isTeamMember {
|
||||
isOrgMember, err := models.IsOrganizationMember(orgID, ctx.User.ID)
|
||||
isOrgMember, err := organization.IsOrganizationMember(ctx, orgID, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOrganizationMember", err)
|
||||
} else if isOrgMember {
|
||||
@@ -377,7 +399,7 @@ func reqOrgMembership() func(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if isMember, err := models.IsOrganizationMember(orgID, ctx.User.ID); err != nil {
|
||||
if isMember, err := organization.IsOrganizationMember(ctx, orgID, ctx.Doer.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOrganizationMember", err)
|
||||
return
|
||||
} else if !isMember {
|
||||
@@ -393,7 +415,7 @@ func reqOrgMembership() func(ctx *context.APIContext) {
|
||||
|
||||
func reqGitHook() func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
if !ctx.User.CanEditGitHook() {
|
||||
if !ctx.Doer.CanEditGitHook() {
|
||||
ctx.Error(http.StatusForbidden, "", "must be allowed to edit Git hooks")
|
||||
return
|
||||
}
|
||||
@@ -426,9 +448,9 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
|
||||
|
||||
var err error
|
||||
if assignOrg {
|
||||
ctx.Org.Organization, err = models.GetOrgByName(ctx.Params(":org"))
|
||||
ctx.Org.Organization, err = organization.GetOrgByName(ctx.Params(":org"))
|
||||
if err != nil {
|
||||
if models.IsErrOrgNotExist(err) {
|
||||
if organization.IsErrOrgNotExist(err) {
|
||||
redirectUserID, err := user_model.LookupUserRedirect(ctx.Params(":org"))
|
||||
if err == nil {
|
||||
context.RedirectToUser(ctx.Context, ctx.Params(":org"), redirectUserID)
|
||||
@@ -442,12 +464,13 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.ContextUser = ctx.Org.Organization.AsUser()
|
||||
}
|
||||
|
||||
if assignTeam {
|
||||
ctx.Org.Team, err = models.GetTeamByID(ctx.ParamsInt64(":teamid"))
|
||||
ctx.Org.Team, err = organization.GetTeamByID(ctx.ParamsInt64(":teamid"))
|
||||
if err != nil {
|
||||
if models.IsErrTeamNotExist(err) {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.NotFound()
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetTeamById", err)
|
||||
@@ -464,7 +487,7 @@ func mustEnableIssues(ctx *context.APIContext) {
|
||||
if ctx.IsSigned {
|
||||
log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+
|
||||
"User in Repo has Permissions: %-+v",
|
||||
ctx.User,
|
||||
ctx.Doer,
|
||||
unit.TypeIssues,
|
||||
ctx.Repo.Repository,
|
||||
ctx.Repo.Permission)
|
||||
@@ -487,7 +510,7 @@ func mustAllowPulls(ctx *context.APIContext) {
|
||||
if ctx.IsSigned {
|
||||
log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+
|
||||
"User in Repo has Permissions: %-+v",
|
||||
ctx.User,
|
||||
ctx.Doer,
|
||||
unit.TypePullRequests,
|
||||
ctx.Repo.Repository,
|
||||
ctx.Repo.Permission)
|
||||
@@ -511,7 +534,7 @@ func mustEnableIssuesOrPulls(ctx *context.APIContext) {
|
||||
if ctx.IsSigned {
|
||||
log.Trace("Permission Denied: User %-v cannot read %-v and %-v in Repo %-v\n"+
|
||||
"User in Repo has Permissions: %-+v",
|
||||
ctx.User,
|
||||
ctx.Doer,
|
||||
unit.TypeIssues,
|
||||
unit.TypePullRequests,
|
||||
ctx.Repo.Repository,
|
||||
@@ -561,11 +584,28 @@ func bind(obj interface{}) http.HandlerFunc {
|
||||
})
|
||||
}
|
||||
|
||||
// Routes registers all v1 APIs routes to web application.
|
||||
func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
|
||||
m := web.NewRoute()
|
||||
// The OAuth2 plugin is expected to be executed first, as it must ignore the user id stored
|
||||
// in the session (if there is a user id stored in session other plugins might return the user
|
||||
// object for that id).
|
||||
//
|
||||
// The Session plugin is expected to be executed second, in order to skip authentication
|
||||
// for users that have already signed in.
|
||||
func buildAuthGroup() *auth.Group {
|
||||
group := auth.NewGroup(
|
||||
&auth.OAuth2{},
|
||||
&auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API
|
||||
)
|
||||
if setting.Service.EnableReverseProxyAuth {
|
||||
group.Add(&auth.ReverseProxy{})
|
||||
}
|
||||
specialAdd(group)
|
||||
|
||||
m.Use(sessioner)
|
||||
return group
|
||||
}
|
||||
|
||||
// Routes registers all v1 APIs routes to web application.
|
||||
func Routes() *web.Route {
|
||||
m := web.NewRoute()
|
||||
|
||||
m.Use(securityHeaders())
|
||||
if setting.CORSConfig.Enabled {
|
||||
@@ -575,14 +615,19 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
|
||||
// setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option
|
||||
AllowedMethods: setting.CORSConfig.Methods,
|
||||
AllowCredentials: setting.CORSConfig.AllowCredentials,
|
||||
AllowedHeaders: []string{"Authorization", "X-CSRFToken", "X-Gitea-OTP"},
|
||||
AllowedHeaders: []string{"Authorization", "X-Gitea-OTP"},
|
||||
MaxAge: int(setting.CORSConfig.MaxAge.Seconds()),
|
||||
}))
|
||||
}
|
||||
m.Use(context.APIContexter())
|
||||
|
||||
group := buildAuthGroup()
|
||||
if err := group.Init(); err != nil {
|
||||
log.Error("Could not initialize '%s' auth method, error: %s", group.Name(), err)
|
||||
}
|
||||
|
||||
// Get user from session if logged in.
|
||||
m.Use(context.APIAuth(auth.NewGroup(auth.Methods()...)))
|
||||
m.Use(context.APIAuth(group))
|
||||
|
||||
m.Use(context.ToggleAPI(&context.ToggleOptions{
|
||||
SignInRequired: setting.Service.RequireSignInView,
|
||||
@@ -641,7 +686,7 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
|
||||
Post(bind(api.CreateAccessTokenOption{}), user.CreateAccessToken)
|
||||
m.Combo("/{id}").Delete(user.DeleteAccessToken)
|
||||
}, reqBasicOrRevProxyAuth())
|
||||
})
|
||||
}, context_service.UserAssignmentAPI())
|
||||
})
|
||||
|
||||
m.Group("/users", func() {
|
||||
@@ -658,7 +703,7 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
|
||||
m.Get("/starred", user.GetStarredRepos)
|
||||
|
||||
m.Get("/subscriptions", user.GetWatchedRepos)
|
||||
})
|
||||
}, context_service.UserAssignmentAPI())
|
||||
}, reqToken())
|
||||
|
||||
m.Group("/user", func() {
|
||||
@@ -674,7 +719,11 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
|
||||
m.Get("/followers", user.ListMyFollowers)
|
||||
m.Group("/following", func() {
|
||||
m.Get("", user.ListMyFollowing)
|
||||
m.Combo("/{username}").Get(user.CheckMyFollowing).Put(user.Follow).Delete(user.Unfollow)
|
||||
m.Group("/{username}", func() {
|
||||
m.Get("", user.CheckMyFollowing)
|
||||
m.Put("", user.Follow)
|
||||
m.Delete("", user.Unfollow)
|
||||
}, context_service.UserAssignmentAPI())
|
||||
})
|
||||
|
||||
m.Group("/keys", func() {
|
||||
@@ -761,14 +810,17 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
|
||||
m.Combo("").Get(repo.GetHook).
|
||||
Patch(bind(api.EditHookOption{}), repo.EditHook).
|
||||
Delete(repo.DeleteHook)
|
||||
m.Post("/tests", context.RepoRefForAPI, repo.TestHook)
|
||||
m.Post("/tests", context.ReferencesGitRepo(), context.RepoRefForAPI, repo.TestHook)
|
||||
})
|
||||
}, reqToken(), reqAdmin(), reqWebhooksEnabled())
|
||||
m.Group("/collaborators", func() {
|
||||
m.Get("", reqAnyRepoReader(), repo.ListCollaborators)
|
||||
m.Combo("/{collaborator}").Get(reqAnyRepoReader(), repo.IsCollaborator).
|
||||
Put(reqAdmin(), bind(api.AddCollaboratorOption{}), repo.AddCollaborator).
|
||||
Delete(reqAdmin(), repo.DeleteCollaborator)
|
||||
m.Group("/{collaborator}", func() {
|
||||
m.Combo("").Get(reqAnyRepoReader(), repo.IsCollaborator).
|
||||
Put(reqAdmin(), bind(api.AddCollaboratorOption{}), repo.AddCollaborator).
|
||||
Delete(reqAdmin(), repo.DeleteCollaborator)
|
||||
m.Get("/permission", repo.GetRepoPermissions)
|
||||
}, reqToken())
|
||||
}, reqToken())
|
||||
m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees)
|
||||
m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers)
|
||||
@@ -778,16 +830,16 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
|
||||
Put(reqAdmin(), repo.AddTeam).
|
||||
Delete(reqAdmin(), repo.DeleteTeam)
|
||||
}, reqToken())
|
||||
m.Get("/raw/*", context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile)
|
||||
m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile)
|
||||
m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive)
|
||||
m.Combo("/forks").Get(repo.ListForks).
|
||||
Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
|
||||
m.Group("/branches", func() {
|
||||
m.Get("", context.ReferencesGitRepo(false), repo.ListBranches)
|
||||
m.Get("/*", context.ReferencesGitRepo(false), repo.GetBranch)
|
||||
m.Delete("/*", reqRepoWriter(unit.TypeCode), context.ReferencesGitRepo(false), repo.DeleteBranch)
|
||||
m.Post("", reqRepoWriter(unit.TypeCode), context.ReferencesGitRepo(false), bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
|
||||
}, reqRepoReader(unit.TypeCode))
|
||||
m.Get("", repo.ListBranches)
|
||||
m.Get("/*", repo.GetBranch)
|
||||
m.Delete("/*", reqRepoWriter(unit.TypeCode), repo.DeleteBranch)
|
||||
m.Post("", reqRepoWriter(unit.TypeCode), bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
|
||||
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
|
||||
m.Group("/branch_protections", func() {
|
||||
m.Get("", repo.ListBranchProtections)
|
||||
m.Post("", bind(api.CreateBranchProtectionOption{}), repo.CreateBranchProtection)
|
||||
@@ -906,10 +958,10 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
|
||||
})
|
||||
m.Group("/releases", func() {
|
||||
m.Combo("").Get(repo.ListReleases).
|
||||
Post(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(false), bind(api.CreateReleaseOption{}), repo.CreateRelease)
|
||||
Post(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.CreateReleaseOption{}), repo.CreateRelease)
|
||||
m.Group("/{id}", func() {
|
||||
m.Combo("").Get(repo.GetRelease).
|
||||
Patch(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(false), bind(api.EditReleaseOption{}), repo.EditRelease).
|
||||
Patch(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.EditReleaseOption{}), repo.EditRelease).
|
||||
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteRelease)
|
||||
m.Group("/assets", func() {
|
||||
m.Combo("").Get(repo.ListReleaseAttachments).
|
||||
@@ -926,7 +978,7 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
|
||||
})
|
||||
}, reqRepoReader(unit.TypeReleases))
|
||||
m.Post("/mirror-sync", reqToken(), reqRepoWriter(unit.TypeCode), repo.MirrorSync)
|
||||
m.Get("/editorconfig/{filename}", context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetEditorconfig)
|
||||
m.Get("/editorconfig/{filename}", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetEditorconfig)
|
||||
m.Group("/pulls", func() {
|
||||
m.Combo("").Get(repo.ListPullRequests).
|
||||
Post(reqToken(), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
|
||||
@@ -937,7 +989,8 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
|
||||
m.Post("/update", reqToken(), repo.UpdatePullRequest)
|
||||
m.Get("/commits", repo.GetPullRequestCommits)
|
||||
m.Combo("/merge").Get(repo.IsPullRequestMerged).
|
||||
Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest)
|
||||
Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest).
|
||||
Delete(reqToken(), mustNotBeArchived, repo.CancelScheduledAutoMerge)
|
||||
m.Group("/reviews", func() {
|
||||
m.Combo("").
|
||||
Get(repo.ListPullReviews).
|
||||
@@ -957,39 +1010,39 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
|
||||
Delete(reqToken(), bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests).
|
||||
Post(reqToken(), bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests)
|
||||
})
|
||||
}, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(false))
|
||||
}, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
|
||||
m.Group("/statuses", func() {
|
||||
m.Combo("/{sha}").Get(repo.GetCommitStatuses).
|
||||
Post(reqToken(), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
|
||||
}, reqRepoReader(unit.TypeCode))
|
||||
m.Group("/commits", func() {
|
||||
m.Get("", context.ReferencesGitRepo(false), repo.GetAllCommits)
|
||||
m.Get("", context.ReferencesGitRepo(), repo.GetAllCommits)
|
||||
m.Group("/{ref}", func() {
|
||||
m.Get("/status", repo.GetCombinedCommitStatusByRef)
|
||||
m.Get("/statuses", repo.GetCommitStatusesByRef)
|
||||
})
|
||||
}, context.ReferencesGitRepo())
|
||||
}, reqRepoReader(unit.TypeCode))
|
||||
m.Group("/git", func() {
|
||||
m.Group("/commits", func() {
|
||||
m.Get("/{sha}", context.ReferencesGitRepo(false), repo.GetSingleCommit)
|
||||
m.Get("/{sha}", repo.GetSingleCommit)
|
||||
m.Get("/{sha}.{diffType:diff|patch}", repo.DownloadCommitDiffOrPatch)
|
||||
})
|
||||
m.Get("/refs", repo.GetGitAllRefs)
|
||||
m.Get("/refs/*", repo.GetGitRefs)
|
||||
m.Get("/trees/{sha}", context.RepoRefForAPI, repo.GetTree)
|
||||
m.Get("/blobs/{sha}", context.RepoRefForAPI, repo.GetBlob)
|
||||
m.Get("/tags/{sha}", context.RepoRefForAPI, repo.GetAnnotatedTag)
|
||||
m.Get("/trees/{sha}", repo.GetTree)
|
||||
m.Get("/blobs/{sha}", repo.GetBlob)
|
||||
m.Get("/tags/{sha}", repo.GetAnnotatedTag)
|
||||
m.Get("/notes/{sha}", repo.GetNote)
|
||||
}, reqRepoReader(unit.TypeCode))
|
||||
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
|
||||
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
|
||||
m.Group("/contents", func() {
|
||||
m.Get("", repo.GetContentsList)
|
||||
m.Get("/*", repo.GetContents)
|
||||
m.Group("/*", func() {
|
||||
m.Post("", bind(api.CreateFileOptions{}), repo.CreateFile)
|
||||
m.Put("", bind(api.UpdateFileOptions{}), repo.UpdateFile)
|
||||
m.Delete("", bind(api.DeleteFileOptions{}), repo.DeleteFile)
|
||||
}, reqRepoWriter(unit.TypeCode), reqToken())
|
||||
m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, repo.CreateFile)
|
||||
m.Put("", bind(api.UpdateFileOptions{}), reqRepoBranchWriter, repo.UpdateFile)
|
||||
m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, repo.DeleteFile)
|
||||
}, reqToken())
|
||||
}, reqRepoReader(unit.TypeCode))
|
||||
m.Get("/signing-key.gpg", misc.SigningKey)
|
||||
m.Group("/topics", func() {
|
||||
@@ -1000,17 +1053,26 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
|
||||
Delete(reqToken(), repo.DeleteTopic)
|
||||
}, reqAdmin())
|
||||
}, reqAnyRepoReader())
|
||||
m.Get("/issue_templates", context.ReferencesGitRepo(false), repo.GetIssueTemplates)
|
||||
m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates)
|
||||
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
|
||||
}, repoAssignment())
|
||||
})
|
||||
|
||||
m.Group("/packages/{username}", func() {
|
||||
m.Group("/{type}/{name}/{version}", func() {
|
||||
m.Get("", packages.GetPackage)
|
||||
m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
|
||||
m.Get("/files", packages.ListPackageFiles)
|
||||
})
|
||||
m.Get("/", packages.ListPackages)
|
||||
}, context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
|
||||
|
||||
// Organizations
|
||||
m.Get("/user/orgs", reqToken(), org.ListMyOrgs)
|
||||
m.Group("/users/{username}/orgs", func() {
|
||||
m.Get("", org.ListUserOrgs)
|
||||
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
|
||||
})
|
||||
}, context_service.UserAssignmentAPI())
|
||||
m.Post("/orgs", reqToken(), bind(api.CreateOrgOption{}), org.Create)
|
||||
m.Get("/orgs", org.GetAll)
|
||||
m.Group("/orgs/{org}", func() {
|
||||
@@ -1065,7 +1127,8 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
|
||||
m.Get("", org.GetTeamRepos)
|
||||
m.Combo("/{org}/{reponame}").
|
||||
Put(org.AddTeamRepository).
|
||||
Delete(org.RemoveTeamRepository)
|
||||
Delete(org.RemoveTeamRepository).
|
||||
Get(org.GetTeamRepo)
|
||||
})
|
||||
}, orgAssignment(false, true), reqToken(), reqTeamMembership())
|
||||
|
||||
@@ -1088,7 +1151,7 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
|
||||
m.Get("/orgs", org.ListUserOrgs)
|
||||
m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
|
||||
m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
|
||||
})
|
||||
}, context_service.UserAssignmentAPI())
|
||||
})
|
||||
m.Group("/unadopted", func() {
|
||||
m.Get("", admin.ListUnadoptedRepositories)
|
||||
|
||||
11
routers/api/v1/auth.go
Normal file
11
routers/api/v1/auth.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package v1
|
||||
|
||||
import auth_service "code.gitea.io/gitea/services/auth"
|
||||
|
||||
func specialAdd(group *auth_service.Group) {}
|
||||
20
routers/api/v1/auth_windows.go
Normal file
20
routers/api/v1/auth_windows.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
auth_service "code.gitea.io/gitea/services/auth"
|
||||
)
|
||||
|
||||
// specialAdd registers the SSPI auth method as the last method in the list.
|
||||
// The SSPI plugin is expected to be executed last, as it returns 401 status code if negotiation
|
||||
// fails (or if negotiation should continue), which would prevent other authentication methods
|
||||
// to execute at all.
|
||||
func specialAdd(group *auth_service.Group) {
|
||||
if auth.IsSSPIEnabled() {
|
||||
group.Add(&auth_service.SSPI{})
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,8 @@ func createContext(req *http.Request) (*context.Context, *httptest.ResponseRecor
|
||||
Render: rnd,
|
||||
Data: make(map[string]interface{}),
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
return c, resp
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,17 @@ package misc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
)
|
||||
|
||||
const cacheKeyNodeInfoUsage = "API_NodeInfoUsage"
|
||||
|
||||
// NodeInfo returns the NodeInfo for the Gitea instance to allow for federation
|
||||
func NodeInfo(ctx *context.APIContext) {
|
||||
// swagger:operation GET /nodeinfo miscellaneous getNodeInfo
|
||||
@@ -23,6 +28,37 @@ func NodeInfo(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/NodeInfo"
|
||||
|
||||
nodeInfoUsage := structs.NodeInfoUsage{}
|
||||
if setting.Federation.ShareUserStatistics {
|
||||
info, ok := ctx.Cache.Get(cacheKeyNodeInfoUsage).(structs.NodeInfoUsage)
|
||||
if !ok {
|
||||
usersTotal := int(user_model.CountUsers(nil))
|
||||
now := time.Now()
|
||||
timeOneMonthAgo := now.AddDate(0, -1, 0).Unix()
|
||||
timeHaveYearAgo := now.AddDate(0, -6, 0).Unix()
|
||||
usersActiveMonth := int(user_model.CountUsers(&user_model.CountUserFilter{LastLoginSince: &timeOneMonthAgo}))
|
||||
usersActiveHalfyear := int(user_model.CountUsers(&user_model.CountUserFilter{LastLoginSince: &timeHaveYearAgo}))
|
||||
|
||||
allIssues, _ := models.CountIssues(&models.IssuesOptions{})
|
||||
allComments, _ := models.CountComments(&models.FindCommentsOptions{})
|
||||
|
||||
info = structs.NodeInfoUsage{
|
||||
Users: structs.NodeInfoUsageUsers{
|
||||
Total: usersTotal,
|
||||
ActiveMonth: usersActiveMonth,
|
||||
ActiveHalfyear: usersActiveHalfyear,
|
||||
},
|
||||
LocalPosts: int(allIssues),
|
||||
LocalComments: int(allComments),
|
||||
}
|
||||
if err := ctx.Cache.Put(cacheKeyNodeInfoUsage, nodeInfoUsage, 180); err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
nodeInfoUsage = info
|
||||
}
|
||||
|
||||
nodeInfo := &structs.NodeInfo{
|
||||
Version: "2.1",
|
||||
Software: structs.NodeInfoSoftware{
|
||||
@@ -34,12 +70,10 @@ func NodeInfo(ctx *context.APIContext) {
|
||||
Protocols: []string{"activitypub"},
|
||||
Services: structs.NodeInfoServices{
|
||||
Inbound: []string{},
|
||||
Outbound: []string{},
|
||||
Outbound: []string{"rss2.0"},
|
||||
},
|
||||
OpenRegistrations: setting.Service.ShowRegistrationButton,
|
||||
Usage: structs.NodeInfoUsage{
|
||||
Users: structs.NodeInfoUsageUsers{},
|
||||
},
|
||||
Usage: nodeInfoUsage,
|
||||
}
|
||||
ctx.JSON(http.StatusOK, nodeInfo)
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// 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 misc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
)
|
||||
|
||||
// tplSwagger swagger page template
|
||||
const tplSwagger base.TplName = "swagger/ui"
|
||||
|
||||
// Swagger render swagger-ui page with v1 json
|
||||
func Swagger(ctx *context.Context) {
|
||||
ctx.Data["APIJSONVersion"] = "v1"
|
||||
ctx.HTML(http.StatusOK, tplSwagger)
|
||||
}
|
||||
@@ -22,18 +22,18 @@ func NewAvailable(ctx *context.APIContext) {
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/NotificationCount"
|
||||
ctx.JSON(http.StatusOK, api.NotificationCount{New: models.CountUnread(ctx.User)})
|
||||
ctx.JSON(http.StatusOK, api.NotificationCount{New: models.CountUnread(ctx.Doer)})
|
||||
}
|
||||
|
||||
func getFindNotificationOptions(ctx *context.APIContext) *models.FindNotificationOptions {
|
||||
before, since, err := utils.GetQueryBeforeSince(ctx)
|
||||
before, since, err := context.GetQueryBeforeSince(ctx.Context)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
|
||||
return nil
|
||||
}
|
||||
opts := &models.FindNotificationOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
UserID: ctx.User.ID,
|
||||
UserID: ctx.Doer.ID,
|
||||
UpdatedBeforeUnix: before,
|
||||
UpdatedAfterUnix: since,
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ func ReadRepoNotifications(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
opts := &models.FindNotificationOptions{
|
||||
UserID: ctx.User.ID,
|
||||
UserID: ctx.Doer.ID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
UpdatedBeforeUnix: lastRead,
|
||||
}
|
||||
@@ -214,10 +214,10 @@ func ReadRepoNotifications(ctx *context.APIContext) {
|
||||
targetStatus = models.NotificationStatusRead
|
||||
}
|
||||
|
||||
changed := make([]*structs.NotificationThread, len(nl))
|
||||
changed := make([]*structs.NotificationThread, 0, len(nl))
|
||||
|
||||
for _, n := range nl {
|
||||
notif, err := models.SetNotificationStatus(n.ID, ctx.User, targetStatus)
|
||||
notif, err := models.SetNotificationStatus(n.ID, ctx.Doer, targetStatus)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
)
|
||||
@@ -87,7 +88,7 @@ func ReadThread(ctx *context.APIContext) {
|
||||
targetStatus = models.NotificationStatusRead
|
||||
}
|
||||
|
||||
notif, err := models.SetNotificationStatus(n.ID, ctx.User, targetStatus)
|
||||
notif, err := models.SetNotificationStatus(n.ID, ctx.Doer, targetStatus)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
@@ -102,14 +103,14 @@ func ReadThread(ctx *context.APIContext) {
|
||||
func getThread(ctx *context.APIContext) *models.Notification {
|
||||
n, err := models.GetNotificationByID(ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if models.IsErrNotExist(err) {
|
||||
if db.IsErrNotExist(err) {
|
||||
ctx.Error(http.StatusNotFound, "GetNotificationByID", err)
|
||||
} else {
|
||||
ctx.InternalServerError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if n.UserID != ctx.User.ID && !ctx.User.IsAdmin {
|
||||
if n.UserID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
|
||||
ctx.Error(http.StatusForbidden, "GetNotificationByID", fmt.Errorf("only user itself and admin are allowed to read/change this thread %d", n.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ func ReadNotifications(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
opts := &models.FindNotificationOptions{
|
||||
UserID: ctx.User.ID,
|
||||
UserID: ctx.Doer.ID,
|
||||
UpdatedBeforeUnix: lastRead,
|
||||
}
|
||||
if !ctx.FormBool("all") {
|
||||
@@ -162,7 +162,7 @@ func ReadNotifications(ctx *context.APIContext) {
|
||||
changed := make([]*structs.NotificationThread, 0, len(nl))
|
||||
|
||||
for _, n := range nl {
|
||||
notif, err := models.SetNotificationStatus(n.ID, ctx.User, targetStatus)
|
||||
notif, err := models.SetNotificationStatus(n.ID, ctx.Doer, targetStatus)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
|
||||
@@ -99,7 +99,7 @@ func CreateLabel(ctx *context.APIContext) {
|
||||
OrgID: ctx.Org.Organization.ID,
|
||||
Description: form.Description,
|
||||
}
|
||||
if err := models.NewLabel(label); err != nil {
|
||||
if err := models.NewLabel(ctx, label); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "NewLabel", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/url"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -19,19 +20,19 @@ import (
|
||||
|
||||
// listMembers list an organization's members
|
||||
func listMembers(ctx *context.APIContext, publicOnly bool) {
|
||||
opts := &models.FindOrgMembersOpts{
|
||||
opts := &organization.FindOrgMembersOpts{
|
||||
OrgID: ctx.Org.Organization.ID,
|
||||
PublicOnly: publicOnly,
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
}
|
||||
|
||||
count, err := models.CountOrgMembers(opts)
|
||||
count, err := organization.CountOrgMembers(opts)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
members, _, err := models.FindOrgMembers(opts)
|
||||
members, _, err := organization.FindOrgMembers(opts)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
@@ -39,7 +40,7 @@ func listMembers(ctx *context.APIContext, publicOnly bool) {
|
||||
|
||||
apiMembers := make([]*api.User, len(members))
|
||||
for i, member := range members {
|
||||
apiMembers[i] = convert.ToUser(member, ctx.User)
|
||||
apiMembers[i] = convert.ToUser(member, ctx.Doer)
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
@@ -72,13 +73,13 @@ func ListMembers(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/UserList"
|
||||
|
||||
publicOnly := true
|
||||
if ctx.User != nil {
|
||||
isMember, err := ctx.Org.Organization.IsOrgMember(ctx.User.ID)
|
||||
if ctx.Doer != nil {
|
||||
isMember, err := ctx.Org.Organization.IsOrgMember(ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOrgMember", err)
|
||||
return
|
||||
}
|
||||
publicOnly = !isMember && !ctx.User.IsAdmin
|
||||
publicOnly = !isMember && !ctx.Doer.IsAdmin
|
||||
}
|
||||
listMembers(ctx, publicOnly)
|
||||
}
|
||||
@@ -130,7 +131,7 @@ func IsMember(ctx *context.APIContext) {
|
||||
// responses:
|
||||
// "204":
|
||||
// description: user is a member
|
||||
// "302":
|
||||
// "303":
|
||||
// description: redirection to /orgs/{org}/public_members/{username}
|
||||
// "404":
|
||||
// description: user is not a member
|
||||
@@ -139,12 +140,12 @@ func IsMember(ctx *context.APIContext) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if ctx.User != nil {
|
||||
userIsMember, err := ctx.Org.Organization.IsOrgMember(ctx.User.ID)
|
||||
if ctx.Doer != nil {
|
||||
userIsMember, err := ctx.Org.Organization.IsOrgMember(ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOrgMember", err)
|
||||
return
|
||||
} else if userIsMember || ctx.User.IsAdmin {
|
||||
} else if userIsMember || ctx.Doer.IsAdmin {
|
||||
userToCheckIsMember, err := ctx.Org.Organization.IsOrgMember(userToCheck.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOrgMember", err)
|
||||
@@ -154,14 +155,14 @@ func IsMember(ctx *context.APIContext) {
|
||||
ctx.NotFound()
|
||||
}
|
||||
return
|
||||
} else if ctx.User.ID == userToCheck.ID {
|
||||
} else if ctx.Doer.ID == userToCheck.ID {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
redirectURL := setting.AppSubURL + "/api/v1/orgs/" + url.PathEscape(ctx.Org.Organization.Name) + "/public_members/" + url.PathEscape(userToCheck.Name)
|
||||
ctx.Redirect(redirectURL, 302)
|
||||
ctx.Redirect(redirectURL)
|
||||
}
|
||||
|
||||
// IsPublicMember check if a user is a public member of an organization
|
||||
@@ -190,7 +191,7 @@ func IsPublicMember(ctx *context.APIContext) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
is, err := models.IsPublicMembership(ctx.Org.Organization.ID, userToCheck.ID)
|
||||
is, err := organization.IsPublicMembership(ctx.Org.Organization.ID, userToCheck.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsPublicMembership", err)
|
||||
return
|
||||
@@ -230,11 +231,11 @@ func PublicizeMember(ctx *context.APIContext) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if userToPublicize.ID != ctx.User.ID {
|
||||
if userToPublicize.ID != ctx.Doer.ID {
|
||||
ctx.Error(http.StatusForbidden, "", "Cannot publicize another member")
|
||||
return
|
||||
}
|
||||
err := models.ChangeOrgUserStatus(ctx.Org.Organization.ID, userToPublicize.ID, true)
|
||||
err := organization.ChangeOrgUserStatus(ctx.Org.Organization.ID, userToPublicize.ID, true)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ChangeOrgUserStatus", err)
|
||||
return
|
||||
@@ -270,11 +271,11 @@ func ConcealMember(ctx *context.APIContext) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if userToConceal.ID != ctx.User.ID {
|
||||
if userToConceal.ID != ctx.Doer.ID {
|
||||
ctx.Error(http.StatusForbidden, "", "Cannot conceal another member")
|
||||
return
|
||||
}
|
||||
err := models.ChangeOrgUserStatus(ctx.Org.Organization.ID, userToConceal.ID, false)
|
||||
err := organization.ChangeOrgUserStatus(ctx.Org.Organization.ID, userToConceal.ID, false)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ChangeOrgUserStatus", err)
|
||||
return
|
||||
@@ -308,8 +309,8 @@ func DeleteMember(ctx *context.APIContext) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if err := ctx.Org.Organization.RemoveMember(member.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "RemoveMember", err)
|
||||
if err := models.RemoveOrgUser(ctx.Org.Organization.ID, member.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "RemoveOrgUser", err)
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ package org
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
@@ -23,19 +23,19 @@ import (
|
||||
|
||||
func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
showPrivate := ctx.IsSigned && (ctx.User.IsAdmin || ctx.User.ID == u.ID)
|
||||
showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == u.ID)
|
||||
|
||||
opts := models.FindOrgOptions{
|
||||
opts := organization.FindOrgOptions{
|
||||
ListOptions: listOptions,
|
||||
UserID: u.ID,
|
||||
IncludePrivate: showPrivate,
|
||||
}
|
||||
orgs, err := models.FindOrgs(opts)
|
||||
orgs, err := organization.FindOrgs(opts)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "FindOrgs", err)
|
||||
return
|
||||
}
|
||||
maxResults, err := models.CountOrgs(opts)
|
||||
maxResults, err := organization.CountOrgs(opts)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CountOrgs", err)
|
||||
return
|
||||
@@ -71,7 +71,7 @@ func ListMyOrgs(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/OrganizationList"
|
||||
|
||||
listUserOrgs(ctx, ctx.User)
|
||||
listUserOrgs(ctx, ctx.Doer)
|
||||
}
|
||||
|
||||
// ListUserOrgs list user's orgs
|
||||
@@ -99,11 +99,7 @@ func ListUserOrgs(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/OrganizationList"
|
||||
|
||||
u := user.GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
listUserOrgs(ctx, u)
|
||||
listUserOrgs(ctx, ctx.ContextUser)
|
||||
}
|
||||
|
||||
// GetUserOrgsPermissions get user permissions in organization
|
||||
@@ -132,11 +128,6 @@ func GetUserOrgsPermissions(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
var u *user_model.User
|
||||
if u = user.GetUserByParams(ctx); u == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var o *user_model.User
|
||||
if o = user.GetUserByParamsName(ctx, ":org"); o == nil {
|
||||
return
|
||||
@@ -144,13 +135,13 @@ func GetUserOrgsPermissions(ctx *context.APIContext) {
|
||||
|
||||
op := api.OrganizationPermissions{}
|
||||
|
||||
if !models.HasOrgOrUserVisible(o, u) {
|
||||
if !organization.HasOrgOrUserVisible(ctx, o, ctx.ContextUser) {
|
||||
ctx.NotFound("HasOrgOrUserVisible", nil)
|
||||
return
|
||||
}
|
||||
|
||||
org := models.OrgFromUser(o)
|
||||
authorizeLevel, err := org.GetOrgUserMaxAuthorizeLevel(u.ID)
|
||||
org := organization.OrgFromUser(o)
|
||||
authorizeLevel, err := org.GetOrgUserMaxAuthorizeLevel(ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetOrgUserAuthorizeLevel", err)
|
||||
return
|
||||
@@ -169,7 +160,7 @@ func GetUserOrgsPermissions(ctx *context.APIContext) {
|
||||
op.IsOwner = true
|
||||
}
|
||||
|
||||
op.CanCreateRepository, err = org.CanCreateOrgRepo(u.ID)
|
||||
op.CanCreateRepository, err = org.CanCreateOrgRepo(ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CanCreateOrgRepo", err)
|
||||
return
|
||||
@@ -201,7 +192,7 @@ func GetAll(ctx *context.APIContext) {
|
||||
vMode := []api.VisibleType{api.VisibleTypePublic}
|
||||
if ctx.IsSigned {
|
||||
vMode = append(vMode, api.VisibleTypeLimited)
|
||||
if ctx.User.IsAdmin {
|
||||
if ctx.Doer.IsAdmin {
|
||||
vMode = append(vMode, api.VisibleTypePrivate)
|
||||
}
|
||||
}
|
||||
@@ -209,7 +200,7 @@ func GetAll(ctx *context.APIContext) {
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
publicOrgs, maxResults, err := user_model.SearchUsers(&user_model.SearchUserOptions{
|
||||
Actor: ctx.User,
|
||||
Actor: ctx.Doer,
|
||||
ListOptions: listOptions,
|
||||
Type: user_model.UserTypeOrganization,
|
||||
OrderBy: db.SearchOrderByAlphabetically,
|
||||
@@ -221,7 +212,7 @@ func GetAll(ctx *context.APIContext) {
|
||||
}
|
||||
orgs := make([]*api.Organization, len(publicOrgs))
|
||||
for i := range publicOrgs {
|
||||
orgs[i] = convert.ToOrganization(models.OrgFromUser(publicOrgs[i]))
|
||||
orgs[i] = convert.ToOrganization(organization.OrgFromUser(publicOrgs[i]))
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
|
||||
@@ -251,7 +242,7 @@ func Create(ctx *context.APIContext) {
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
form := web.GetForm(ctx).(*api.CreateOrgOption)
|
||||
if !ctx.User.CanCreateOrganization() {
|
||||
if !ctx.Doer.CanCreateOrganization() {
|
||||
ctx.Error(http.StatusForbidden, "Create organization not allowed", nil)
|
||||
return
|
||||
}
|
||||
@@ -261,7 +252,7 @@ func Create(ctx *context.APIContext) {
|
||||
visibility = api.VisibilityModes[form.Visibility]
|
||||
}
|
||||
|
||||
org := &models.Organization{
|
||||
org := &organization.Organization{
|
||||
Name: form.UserName,
|
||||
FullName: form.FullName,
|
||||
Description: form.Description,
|
||||
@@ -272,7 +263,7 @@ func Create(ctx *context.APIContext) {
|
||||
Visibility: visibility,
|
||||
RepoAdminChangeTeamAccess: form.RepoAdminChangeTeamAccess,
|
||||
}
|
||||
if err := models.CreateOrganization(org, ctx.User); err != nil {
|
||||
if err := organization.CreateOrganization(org, ctx.Doer); err != nil {
|
||||
if user_model.IsErrUserAlreadyExist(err) ||
|
||||
db.IsErrNameReserved(err) ||
|
||||
db.IsErrNameCharsNotAllowed(err) ||
|
||||
@@ -304,7 +295,7 @@ func Get(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/Organization"
|
||||
|
||||
if !models.HasOrgOrUserVisible(ctx.Org.Organization.AsUser(), ctx.User) {
|
||||
if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
|
||||
ctx.NotFound("HasOrgOrUserVisible", nil)
|
||||
return
|
||||
}
|
||||
@@ -346,7 +337,7 @@ func Edit(ctx *context.APIContext) {
|
||||
if form.RepoAdminChangeTeamAccess != nil {
|
||||
org.RepoAdminChangeTeamAccess = *form.RepoAdminChangeTeamAccess
|
||||
}
|
||||
if err := user_model.UpdateUserCols(db.DefaultContext, org.AsUser(),
|
||||
if err := user_model.UpdateUserCols(ctx, org.AsUser(),
|
||||
"full_name", "description", "website", "location",
|
||||
"visibility", "repo_admin_change_team_access",
|
||||
); err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
@@ -47,7 +48,7 @@ func ListTeams(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/TeamList"
|
||||
|
||||
teams, count, err := models.SearchOrgTeams(&models.SearchOrgTeamOptions{
|
||||
teams, count, err := organization.SearchTeam(&organization.SearchTeamOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
OrgID: ctx.Org.Organization.ID,
|
||||
})
|
||||
@@ -90,9 +91,9 @@ func ListUserTeams(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/TeamList"
|
||||
|
||||
teams, count, err := models.GetUserTeams(&models.GetUserTeamOptions{
|
||||
teams, count, err := organization.SearchTeam(&organization.SearchTeamOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
UserID: ctx.User.ID,
|
||||
UserID: ctx.Doer.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserTeams", err)
|
||||
@@ -104,7 +105,7 @@ func ListUserTeams(ctx *context.APIContext) {
|
||||
for i := range teams {
|
||||
apiOrg, ok := cache[teams[i].OrgID]
|
||||
if !ok {
|
||||
org, err := models.GetOrgByID(teams[i].OrgID)
|
||||
org, err := organization.GetOrgByID(teams[i].OrgID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserByID", err)
|
||||
return
|
||||
@@ -150,11 +151,11 @@ func GetTeam(ctx *context.APIContext) {
|
||||
ctx.JSON(http.StatusOK, convert.ToTeam(ctx.Org.Team))
|
||||
}
|
||||
|
||||
func attachTeamUnits(team *models.Team, units []string) {
|
||||
func attachTeamUnits(team *organization.Team, units []string) {
|
||||
unitTypes := unit_model.FindUnitTypes(units...)
|
||||
team.Units = make([]*models.TeamUnit, 0, len(units))
|
||||
team.Units = make([]*organization.TeamUnit, 0, len(units))
|
||||
for _, tp := range unitTypes {
|
||||
team.Units = append(team.Units, &models.TeamUnit{
|
||||
team.Units = append(team.Units, &organization.TeamUnit{
|
||||
OrgID: team.OrgID,
|
||||
Type: tp,
|
||||
AccessMode: team.AccessMode,
|
||||
@@ -170,10 +171,10 @@ func convertUnitsMap(unitsMap map[string]string) map[unit_model.Type]perm.Access
|
||||
return res
|
||||
}
|
||||
|
||||
func attachTeamUnitsMap(team *models.Team, unitsMap map[string]string) {
|
||||
team.Units = make([]*models.TeamUnit, 0, len(unitsMap))
|
||||
func attachTeamUnitsMap(team *organization.Team, unitsMap map[string]string) {
|
||||
team.Units = make([]*organization.TeamUnit, 0, len(unitsMap))
|
||||
for unitKey, p := range unitsMap {
|
||||
team.Units = append(team.Units, &models.TeamUnit{
|
||||
team.Units = append(team.Units, &organization.TeamUnit{
|
||||
OrgID: team.OrgID,
|
||||
Type: unit_model.TypeFromKey(unitKey),
|
||||
AccessMode: perm.ParseAccessMode(p),
|
||||
@@ -210,7 +211,7 @@ func CreateTeam(ctx *context.APIContext) {
|
||||
if p < perm.AccessModeAdmin && len(form.UnitsMap) > 0 {
|
||||
p = unit_model.MinUnitAccessMode(convertUnitsMap(form.UnitsMap))
|
||||
}
|
||||
team := &models.Team{
|
||||
team := &organization.Team{
|
||||
OrgID: ctx.Org.Organization.ID,
|
||||
Name: form.Name,
|
||||
Description: form.Description,
|
||||
@@ -231,7 +232,7 @@ func CreateTeam(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if err := models.NewTeam(team); err != nil {
|
||||
if models.IsErrTeamAlreadyExist(err) {
|
||||
if organization.IsErrTeamAlreadyExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "NewTeam", err)
|
||||
@@ -368,24 +369,27 @@ func GetTeamMembers(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
|
||||
isMember, err := models.IsOrganizationMember(ctx.Org.Team.OrgID, ctx.User.ID)
|
||||
isMember, err := organization.IsOrganizationMember(ctx, ctx.Org.Team.OrgID, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOrganizationMember", err)
|
||||
return
|
||||
} else if !isMember && !ctx.User.IsAdmin {
|
||||
} else if !isMember && !ctx.Doer.IsAdmin {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if err := ctx.Org.Team.GetMembers(&models.SearchMembersOptions{
|
||||
teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
}); err != nil {
|
||||
TeamID: ctx.Org.Team.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetTeamMembers", err)
|
||||
return
|
||||
}
|
||||
members := make([]*api.User, len(ctx.Org.Team.Members))
|
||||
for i, member := range ctx.Org.Team.Members {
|
||||
members[i] = convert.ToUser(member, ctx.User)
|
||||
|
||||
members := make([]*api.User, len(teamMembers))
|
||||
for i, member := range teamMembers {
|
||||
members[i] = convert.ToUser(member, ctx.Doer)
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(int64(ctx.Org.Team.NumMembers))
|
||||
@@ -422,7 +426,7 @@ func GetTeamMember(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
teamID := ctx.ParamsInt64("teamid")
|
||||
isTeamMember, err := models.IsUserInTeams(u.ID, []int64{teamID})
|
||||
isTeamMember, err := organization.IsUserInTeams(ctx, u.ID, []int64{teamID})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsUserInTeams", err)
|
||||
return
|
||||
@@ -430,7 +434,7 @@ func GetTeamMember(ctx *context.APIContext) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToUser(u, ctx.User))
|
||||
ctx.JSON(http.StatusOK, convert.ToUser(u, ctx.Doer))
|
||||
}
|
||||
|
||||
// AddTeamMember api for add a member to a team
|
||||
@@ -462,7 +466,7 @@ func AddTeamMember(ctx *context.APIContext) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if err := ctx.Org.Team.AddMember(u.ID); err != nil {
|
||||
if err := models.AddTeamMember(ctx.Org.Team, u.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "AddMember", err)
|
||||
return
|
||||
}
|
||||
@@ -499,8 +503,8 @@ func RemoveTeamMember(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := ctx.Org.Team.RemoveMember(u.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "RemoveMember", err)
|
||||
if err := models.RemoveTeamMember(ctx.Org.Team, u.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "RemoveTeamMember", err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
@@ -533,14 +537,17 @@ func GetTeamRepos(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/RepositoryList"
|
||||
|
||||
team := ctx.Org.Team
|
||||
if err := team.GetRepositories(&models.SearchOrgTeamOptions{
|
||||
teamRepos, err := organization.GetTeamRepositories(ctx, &organization.SearchTeamRepoOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
}); err != nil {
|
||||
TeamID: team.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err)
|
||||
return
|
||||
}
|
||||
repos := make([]*api.Repository, len(team.Repos))
|
||||
for i, repo := range team.Repos {
|
||||
access, err := models.AccessLevel(ctx.User, repo)
|
||||
repos := make([]*api.Repository, len(teamRepos))
|
||||
for i, repo := range teamRepos {
|
||||
access, err := models.AccessLevel(ctx.Doer, repo)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err)
|
||||
return
|
||||
@@ -551,6 +558,55 @@ func GetTeamRepos(ctx *context.APIContext) {
|
||||
ctx.JSON(http.StatusOK, repos)
|
||||
}
|
||||
|
||||
// GetTeamRepo api for get a particular repo of team
|
||||
func GetTeamRepo(ctx *context.APIContext) {
|
||||
// swagger:operation GET /teams/{id}/repos/{org}/{repo} organization orgListTeamRepo
|
||||
// ---
|
||||
// summary: List a particular repo of team
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the team
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: organization that owns the repo to list
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo to list
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Repository"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
repo := getRepositoryByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if !organization.HasTeamRepo(ctx, ctx.Org.Team.OrgID, ctx.Org.Team.ID, repo.ID) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
access, err := models.AccessLevel(ctx.Doer, repo)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToRepo(repo, access))
|
||||
}
|
||||
|
||||
// getRepositoryByParams get repository by a team's organization ID and repo name
|
||||
func getRepositoryByParams(ctx *context.APIContext) *repo_model.Repository {
|
||||
repo, err := repo_model.GetRepositoryByName(ctx.Org.Team.OrgID, ctx.Params(":reponame"))
|
||||
@@ -599,14 +655,14 @@ func AddTeamRepository(ctx *context.APIContext) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if access, err := models.AccessLevel(ctx.User, repo); err != nil {
|
||||
if access, err := models.AccessLevel(ctx.Doer, repo); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "AccessLevel", err)
|
||||
return
|
||||
} else if access < perm.AccessModeAdmin {
|
||||
ctx.Error(http.StatusForbidden, "", "Must have admin-level access to the repository")
|
||||
return
|
||||
}
|
||||
if err := ctx.Org.Team.AddRepository(repo); err != nil {
|
||||
if err := models.AddRepository(ctx.Org.Team, repo); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "AddRepository", err)
|
||||
return
|
||||
}
|
||||
@@ -649,14 +705,14 @@ func RemoveTeamRepository(ctx *context.APIContext) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if access, err := models.AccessLevel(ctx.User, repo); err != nil {
|
||||
if access, err := models.AccessLevel(ctx.Doer, repo); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "AccessLevel", err)
|
||||
return
|
||||
} else if access < perm.AccessModeAdmin {
|
||||
ctx.Error(http.StatusForbidden, "", "Must have admin-level access to the repository")
|
||||
return
|
||||
}
|
||||
if err := ctx.Org.Team.RemoveRepository(repo.ID); err != nil {
|
||||
if err := models.RemoveRepository(ctx.Org.Team, repo.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "RemoveRepository", err)
|
||||
return
|
||||
}
|
||||
@@ -707,14 +763,15 @@ func SearchTeam(ctx *context.APIContext) {
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
opts := &models.SearchOrgTeamOptions{
|
||||
opts := &organization.SearchTeamOptions{
|
||||
UserID: ctx.Doer.ID,
|
||||
Keyword: ctx.FormTrim("q"),
|
||||
OrgID: ctx.Org.Organization.ID,
|
||||
IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"),
|
||||
ListOptions: listOptions,
|
||||
}
|
||||
|
||||
teams, maxResults, err := models.SearchOrgTeams(opts)
|
||||
teams, maxResults, err := organization.SearchTeam(opts)
|
||||
if err != nil {
|
||||
log.Error("SearchTeam failed: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
|
||||
|
||||
212
routers/api/v1/packages/package.go
Normal file
212
routers/api/v1/packages/package.go
Normal file
@@ -0,0 +1,212 @@
|
||||
// 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 (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/packages"
|
||||
"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"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
// ListPackages gets all packages of an owner
|
||||
func ListPackages(ctx *context.APIContext) {
|
||||
// swagger:operation GET /packages/{owner} package listPackages
|
||||
// ---
|
||||
// summary: Gets all packages of an owner
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the packages
|
||||
// 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: type
|
||||
// in: query
|
||||
// description: package type filter
|
||||
// type: string
|
||||
// enum: [composer, conan, container, generic, helm, maven, npm, nuget, pypi, rubygems]
|
||||
// - name: q
|
||||
// in: query
|
||||
// description: name filter
|
||||
// type: string
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/PackageList"
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
packageType := ctx.FormTrim("type")
|
||||
query := ctx.FormTrim("q")
|
||||
|
||||
pvs, count, err := packages.SearchVersions(ctx, &packages.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages.Type(packageType),
|
||||
Name: packages.SearchValue{Value: query},
|
||||
Paginator: &listOptions,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "SearchVersions", err)
|
||||
return
|
||||
}
|
||||
|
||||
pds, err := packages.GetPackageDescriptors(ctx, pvs)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetPackageDescriptors", err)
|
||||
return
|
||||
}
|
||||
|
||||
apiPackages := make([]*api.Package, 0, len(pds))
|
||||
for _, pd := range pds {
|
||||
apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "Error converting package for api", err)
|
||||
return
|
||||
}
|
||||
apiPackages = append(apiPackages, apiPackage)
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(int(count), listOptions.PageSize)
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, apiPackages)
|
||||
}
|
||||
|
||||
// GetPackage gets a package
|
||||
func GetPackage(ctx *context.APIContext) {
|
||||
// swagger:operation GET /packages/{owner}/{type}/{name}/{version} package getPackage
|
||||
// ---
|
||||
// summary: Gets a package
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: type
|
||||
// in: path
|
||||
// description: type of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: name
|
||||
// in: path
|
||||
// description: name of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: version
|
||||
// in: path
|
||||
// description: version of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Package"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
apiPackage, err := convert.ToPackage(ctx, ctx.Package.Descriptor, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "Error converting package for api", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, apiPackage)
|
||||
}
|
||||
|
||||
// DeletePackage deletes a package
|
||||
func DeletePackage(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /packages/{owner}/{type}/{name}/{version} package deletePackage
|
||||
// ---
|
||||
// summary: Delete a package
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: type
|
||||
// in: path
|
||||
// description: type of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: name
|
||||
// in: path
|
||||
// description: name of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: version
|
||||
// in: path
|
||||
// description: version of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
err := packages_service.RemovePackageVersion(ctx.Doer, ctx.Package.Descriptor.Version)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "RemovePackageVersion", err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ListPackageFiles gets all files of a package
|
||||
func ListPackageFiles(ctx *context.APIContext) {
|
||||
// swagger:operation GET /packages/{owner}/{type}/{name}/{version}/files package listPackageFiles
|
||||
// ---
|
||||
// summary: Gets all files of a package
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: type
|
||||
// in: path
|
||||
// description: type of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: name
|
||||
// in: path
|
||||
// description: name of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: version
|
||||
// in: path
|
||||
// description: version of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/PackageFileList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
apiPackageFiles := make([]*api.PackageFile, 0, len(ctx.Package.Descriptor.Files))
|
||||
for _, pfd := range ctx.Package.Descriptor.Files {
|
||||
apiPackageFiles = append(apiPackageFiles, convert.ToPackageFile(pfd))
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, apiPackageFiles)
|
||||
}
|
||||
@@ -45,7 +45,8 @@ func GetBlob(ctx *context.APIContext) {
|
||||
ctx.Error(http.StatusBadRequest, "", "sha not provided")
|
||||
return
|
||||
}
|
||||
if blob, err := files_service.GetBlobBySHA(ctx, ctx.Repo.Repository, sha); err != nil {
|
||||
|
||||
if blob, err := files_service.GetBlobBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil {
|
||||
ctx.Error(http.StatusBadRequest, "", err)
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, blob)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
@@ -75,7 +76,7 @@ func GetBranch(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
br, err := convert.ToBranch(ctx.Repo.Repository, branch, c, branchProtection, ctx.User, ctx.Repo.IsAdmin())
|
||||
br, err := convert.ToBranch(ctx.Repo.Repository, branch, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
|
||||
return
|
||||
@@ -117,7 +118,7 @@ func DeleteBranch(ctx *context.APIContext) {
|
||||
|
||||
branchName := ctx.Params("*")
|
||||
|
||||
if err := repo_service.DeleteBranch(ctx.User, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
|
||||
if err := repo_service.DeleteBranch(ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
|
||||
switch {
|
||||
case git.IsErrBranchNotExist(err):
|
||||
ctx.NotFound(err)
|
||||
@@ -176,7 +177,7 @@ func CreateBranch(ctx *context.APIContext) {
|
||||
opt.OldBranchName = ctx.Repo.Repository.DefaultBranch
|
||||
}
|
||||
|
||||
err := repo_service.CreateNewBranch(ctx, ctx.User, ctx.Repo.Repository, opt.OldBranchName, opt.BranchName)
|
||||
err := repo_service.CreateNewBranch(ctx, ctx.Doer, ctx.Repo.Repository, opt.OldBranchName, opt.BranchName)
|
||||
if err != nil {
|
||||
if models.IsErrBranchDoesNotExist(err) {
|
||||
ctx.Error(http.StatusNotFound, "", "The old branch does not exist")
|
||||
@@ -211,7 +212,7 @@ func CreateBranch(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
br, err := convert.ToBranch(ctx.Repo.Repository, branch, commit, branchProtection, ctx.User, ctx.Repo.IsAdmin())
|
||||
br, err := convert.ToBranch(ctx.Repo.Repository, branch, commit, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
|
||||
return
|
||||
@@ -258,10 +259,15 @@ func ListBranches(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
apiBranches := make([]*api.Branch, len(branches))
|
||||
apiBranches := make([]*api.Branch, 0, len(branches))
|
||||
for i := range branches {
|
||||
c, err := branches[i].GetCommit()
|
||||
if err != nil {
|
||||
// Skip if this branch doesn't exist anymore.
|
||||
if git.IsErrNotExist(err) {
|
||||
totalNumOfBranches--
|
||||
continue
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "GetCommit", err)
|
||||
return
|
||||
}
|
||||
@@ -270,11 +276,12 @@ func ListBranches(ctx *context.APIContext) {
|
||||
ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err)
|
||||
return
|
||||
}
|
||||
apiBranches[i], err = convert.ToBranch(ctx.Repo.Repository, branches[i], c, branchProtection, ctx.User, ctx.Repo.IsAdmin())
|
||||
apiBranch, err := convert.ToBranch(ctx.Repo.Repository, branches[i], c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
|
||||
return
|
||||
}
|
||||
apiBranches = append(apiBranches, apiBranch)
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(totalNumOfBranches, listOptions.PageSize)
|
||||
@@ -448,27 +455,27 @@ func CreateBranchProtection(ctx *context.APIContext) {
|
||||
}
|
||||
var whitelistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
|
||||
if repo.Owner.IsOrganization() {
|
||||
whitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.PushWhitelistTeams, false)
|
||||
whitelistTeams, err = organization.GetTeamIDsByNames(repo.OwnerID, form.PushWhitelistTeams, false)
|
||||
if err != nil {
|
||||
if models.IsErrTeamNotExist(err) {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
|
||||
return
|
||||
}
|
||||
mergeWhitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.MergeWhitelistTeams, false)
|
||||
mergeWhitelistTeams, err = organization.GetTeamIDsByNames(repo.OwnerID, form.MergeWhitelistTeams, false)
|
||||
if err != nil {
|
||||
if models.IsErrTeamNotExist(err) {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
|
||||
return
|
||||
}
|
||||
approvalsWhitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.ApprovalsWhitelistTeams, false)
|
||||
approvalsWhitelistTeams, err = organization.GetTeamIDsByNames(repo.OwnerID, form.ApprovalsWhitelistTeams, false)
|
||||
if err != nil {
|
||||
if models.IsErrTeamNotExist(err) {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
|
||||
return
|
||||
}
|
||||
@@ -497,7 +504,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
|
||||
BlockOnOutdatedBranch: form.BlockOnOutdatedBranch,
|
||||
}
|
||||
|
||||
err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
|
||||
err = models.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
|
||||
UserIDs: whitelistUsers,
|
||||
TeamIDs: whitelistTeams,
|
||||
MergeUserIDs: mergeWhitelistUsers,
|
||||
@@ -692,9 +699,9 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
var whitelistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
|
||||
if repo.Owner.IsOrganization() {
|
||||
if form.PushWhitelistTeams != nil {
|
||||
whitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.PushWhitelistTeams, false)
|
||||
whitelistTeams, err = organization.GetTeamIDsByNames(repo.OwnerID, form.PushWhitelistTeams, false)
|
||||
if err != nil {
|
||||
if models.IsErrTeamNotExist(err) {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
|
||||
return
|
||||
}
|
||||
@@ -705,9 +712,9 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
whitelistTeams = protectBranch.WhitelistTeamIDs
|
||||
}
|
||||
if form.MergeWhitelistTeams != nil {
|
||||
mergeWhitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.MergeWhitelistTeams, false)
|
||||
mergeWhitelistTeams, err = organization.GetTeamIDsByNames(repo.OwnerID, form.MergeWhitelistTeams, false)
|
||||
if err != nil {
|
||||
if models.IsErrTeamNotExist(err) {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
|
||||
return
|
||||
}
|
||||
@@ -718,9 +725,9 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
mergeWhitelistTeams = protectBranch.MergeWhitelistTeamIDs
|
||||
}
|
||||
if form.ApprovalsWhitelistTeams != nil {
|
||||
approvalsWhitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.ApprovalsWhitelistTeams, false)
|
||||
approvalsWhitelistTeams, err = organization.GetTeamIDsByNames(repo.OwnerID, form.ApprovalsWhitelistTeams, false)
|
||||
if err != nil {
|
||||
if models.IsErrTeamNotExist(err) {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
|
||||
return
|
||||
}
|
||||
@@ -732,7 +739,7 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
|
||||
err = models.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
|
||||
UserIDs: whitelistUsers,
|
||||
TeamIDs: whitelistTeams,
|
||||
MergeUserIDs: mergeWhitelistUsers,
|
||||
|
||||
@@ -63,7 +63,7 @@ func ListCollaborators(ctx *context.APIContext) {
|
||||
|
||||
users := make([]*api.User, len(collaborators))
|
||||
for i, collaborator := range collaborators {
|
||||
users[i] = convert.ToUser(collaborator.User, ctx.User)
|
||||
users[i] = convert.ToUser(collaborator.User, ctx.Doer)
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
@@ -233,6 +233,61 @@ func DeleteCollaborator(ctx *context.APIContext) {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GetRepoPermissions gets repository permissions for a user
|
||||
func GetRepoPermissions(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/collaborators/{collaborator}/permission repository repoGetRepoPermissions
|
||||
// ---
|
||||
// summary: Get repository permissions for a user
|
||||
// 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: collaborator
|
||||
// in: path
|
||||
// description: username of the collaborator
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/RepoCollaboratorPermission"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
|
||||
if !ctx.Doer.IsAdmin && ctx.Doer.LoginName != ctx.Params(":collaborator") && !ctx.IsUserRepoAdmin() {
|
||||
ctx.Error(http.StatusForbidden, "User", "Only admins can query all permissions, repo admins can query all repo permissions, collaborators can query only their own")
|
||||
return
|
||||
}
|
||||
|
||||
collaborator, err := user_model.GetUserByName(ctx.Params(":collaborator"))
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.Error(http.StatusNotFound, "GetUserByName", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
permission, err := models.GetUserRepoPermission(ctx, ctx.Repo.Repository, collaborator)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToUserAndPermission(collaborator, ctx.ContextUser, permission.AccessMode))
|
||||
}
|
||||
|
||||
// GetReviewers return all users that can be requested to review in this repo
|
||||
func GetReviewers(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/reviewers repository repoGetReviewers
|
||||
@@ -255,12 +310,12 @@ func GetReviewers(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
|
||||
reviewers, err := models.GetReviewers(ctx.Repo.Repository, ctx.User.ID, 0)
|
||||
reviewers, err := models.GetReviewers(ctx.Repo.Repository, ctx.Doer.ID, 0)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToUsers(ctx.User, reviewers))
|
||||
ctx.JSON(http.StatusOK, convert.ToUsers(ctx.Doer, reviewers))
|
||||
}
|
||||
|
||||
// GetAssignees return all users that have write access and can be assigned to issues
|
||||
@@ -290,5 +345,5 @@ func GetAssignees(ctx *context.APIContext) {
|
||||
ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToUsers(ctx.User, assignees))
|
||||
ctx.JSON(http.StatusOK, convert.ToUsers(ctx.Doer, assignees))
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
@@ -268,16 +267,12 @@ func DownloadCommitDiffOrPatch(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/string"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
repoPath := repo_model.RepoPath(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
|
||||
if err := git.GetRawDiff(
|
||||
ctx,
|
||||
repoPath,
|
||||
ctx.Params(":sha"),
|
||||
git.RawDiffType(ctx.Params(":diffType")),
|
||||
ctx.Resp,
|
||||
); err != nil {
|
||||
sha := ctx.Params(":sha")
|
||||
diffType := git.RawDiffType(ctx.Params(":diffType"))
|
||||
|
||||
if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, diffType, ctx.Resp); err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.NotFound(ctx.Params(":sha"))
|
||||
ctx.NotFound(sha)
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "DownloadCommitDiffOrPatch", err)
|
||||
|
||||
@@ -9,13 +9,16 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
@@ -53,7 +56,7 @@ func GetRawFile(ctx *context.APIContext) {
|
||||
// required: false
|
||||
// responses:
|
||||
// 200:
|
||||
// description: success
|
||||
// description: Returns raw file content.
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
@@ -62,33 +65,50 @@ func GetRawFile(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
commit := ctx.Repo.Commit
|
||||
|
||||
if ref := ctx.FormTrim("ref"); len(ref) > 0 {
|
||||
var err error
|
||||
commit, err = ctx.Repo.GitRepo.GetCommit(ref)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.NotFound()
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetBlobByPath", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
blob, lastModified := getBlobForEntry(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
blob, err := commit.GetBlobByPath(ctx.Repo.TreePath)
|
||||
if err := common.ServeBlob(ctx.Context, blob, lastModified); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
|
||||
}
|
||||
}
|
||||
|
||||
func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, lastModified time.Time) {
|
||||
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.NotFound()
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetBlobByPath", err)
|
||||
ctx.Error(http.StatusInternalServerError, "GetTreeEntryByPath", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err = common.ServeBlob(ctx.Context, blob); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
|
||||
|
||||
if entry.IsDir() || entry.IsSubModule() {
|
||||
ctx.NotFound("getBlobForEntry", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var c *git.LastCommitCache
|
||||
if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount {
|
||||
c = git.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, setting.LastCommitCacheTTLSeconds, cache.GetCache())
|
||||
}
|
||||
|
||||
info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:], c)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetCommitsInfo", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(info) == 1 {
|
||||
// Not Modified
|
||||
lastModified = info[0].Commit.Committer.When
|
||||
}
|
||||
blob = entry.Blob()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetArchive get archive of a repository
|
||||
@@ -122,7 +142,7 @@ func GetArchive(ctx *context.APIContext) {
|
||||
|
||||
repoPath := repo_model.RepoPath(ctx.Params(":username"), ctx.Params(":reponame"))
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
gitRepo, err := git.OpenRepositoryCtx(ctx, repoPath)
|
||||
gitRepo, err := git.OpenRepository(ctx, repoPath)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
|
||||
return
|
||||
@@ -157,13 +177,18 @@ func GetEditorconfig(ctx *context.APIContext) {
|
||||
// description: filepath of file to get
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: query
|
||||
// description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
|
||||
// type: string
|
||||
// required: false
|
||||
// responses:
|
||||
// 200:
|
||||
// description: success
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
ec, err := ctx.Repo.GetEditorconfig()
|
||||
ec, err := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.NotFound(err)
|
||||
@@ -183,8 +208,10 @@ func GetEditorconfig(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
// canWriteFiles returns true if repository is editable and user has proper access level.
|
||||
func canWriteFiles(r *context.Repository) bool {
|
||||
return r.Permission.CanWrite(unit.TypeCode) && !r.Repository.IsMirror && !r.Repository.IsArchived
|
||||
func canWriteFiles(ctx *context.APIContext, branch string) bool {
|
||||
return ctx.Repo.Permission.CanWriteToBranch(ctx.Doer, branch) &&
|
||||
!ctx.Repo.Repository.IsMirror &&
|
||||
!ctx.Repo.Repository.IsArchived
|
||||
}
|
||||
|
||||
// canReadFiles returns true if repository is readable and user has proper access level.
|
||||
@@ -233,9 +260,6 @@ func CreateFile(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
apiOpts := web.GetForm(ctx).(*api.CreateFileOptions)
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
|
||||
}
|
||||
|
||||
if apiOpts.BranchName == "" {
|
||||
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
||||
@@ -389,9 +413,9 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) {
|
||||
|
||||
// Called from both CreateFile or UpdateFile to handle both
|
||||
func createOrUpdateFile(ctx *context.APIContext, opts *files_service.UpdateRepoFileOptions) (*api.FileResponse, error) {
|
||||
if !canWriteFiles(ctx.Repo) {
|
||||
if !canWriteFiles(ctx, opts.OldBranch) {
|
||||
return nil, models.ErrUserDoesNotHaveAccessToRepo{
|
||||
UserID: ctx.User.ID,
|
||||
UserID: ctx.Doer.ID,
|
||||
RepoName: ctx.Repo.Repository.LowerName,
|
||||
}
|
||||
}
|
||||
@@ -402,7 +426,7 @@ func createOrUpdateFile(ctx *context.APIContext, opts *files_service.UpdateRepoF
|
||||
}
|
||||
opts.Content = string(content)
|
||||
|
||||
return files_service.CreateOrUpdateRepoFile(ctx, ctx.Repo.Repository, ctx.User, opts)
|
||||
return files_service.CreateOrUpdateRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, opts)
|
||||
}
|
||||
|
||||
// DeleteFile Delete a file in a repository
|
||||
@@ -446,9 +470,9 @@ func DeleteFile(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
apiOpts := web.GetForm(ctx).(*api.DeleteFileOptions)
|
||||
if !canWriteFiles(ctx.Repo) {
|
||||
if !canWriteFiles(ctx, apiOpts.BranchName) {
|
||||
ctx.Error(http.StatusForbidden, "DeleteFile", models.ErrUserDoesNotHaveAccessToRepo{
|
||||
UserID: ctx.User.ID,
|
||||
UserID: ctx.Doer.ID,
|
||||
RepoName: ctx.Repo.Repository.LowerName,
|
||||
})
|
||||
return
|
||||
@@ -489,7 +513,7 @@ func DeleteFile(ctx *context.APIContext) {
|
||||
opts.Message = ctx.Tr("repo.editor.delete", opts.TreePath)
|
||||
}
|
||||
|
||||
if fileResponse, err := files_service.DeleteRepoFile(ctx, ctx.Repo.Repository, ctx.User, opts); err != nil {
|
||||
if fileResponse, err := files_service.DeleteRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
|
||||
if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
|
||||
ctx.Error(http.StatusNotFound, "DeleteFile", err)
|
||||
return
|
||||
@@ -546,7 +570,7 @@ func GetContents(ctx *context.APIContext) {
|
||||
|
||||
if !canReadFiles(ctx.Repo) {
|
||||
ctx.Error(http.StatusInternalServerError, "GetContentsOrList", models.ErrUserDoesNotHaveAccessToRepo{
|
||||
UserID: ctx.User.ID,
|
||||
UserID: ctx.Doer.ID,
|
||||
RepoName: ctx.Repo.Repository.LowerName,
|
||||
})
|
||||
return
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@@ -58,7 +59,7 @@ func ListForks(ctx *context.APIContext) {
|
||||
}
|
||||
apiForks := make([]*api.Repository, len(forks))
|
||||
for i, fork := range forks {
|
||||
access, err := models.AccessLevel(ctx.User, fork)
|
||||
access, err := models.AccessLevel(ctx.Doer, fork)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "AccessLevel", err)
|
||||
return
|
||||
@@ -106,18 +107,18 @@ func CreateFork(ctx *context.APIContext) {
|
||||
repo := ctx.Repo.Repository
|
||||
var forker *user_model.User // user/org that will own the fork
|
||||
if form.Organization == nil {
|
||||
forker = ctx.User
|
||||
forker = ctx.Doer
|
||||
} else {
|
||||
org, err := models.GetOrgByName(*form.Organization)
|
||||
org, err := organization.GetOrgByName(*form.Organization)
|
||||
if err != nil {
|
||||
if models.IsErrOrgNotExist(err) {
|
||||
if organization.IsErrOrgNotExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetOrgByName", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
isMember, err := org.IsOrgMember(ctx.User.ID)
|
||||
isMember, err := org.IsOrgMember(ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOrgMember", err)
|
||||
return
|
||||
@@ -135,7 +136,7 @@ func CreateFork(ctx *context.APIContext) {
|
||||
name = *form.Name
|
||||
}
|
||||
|
||||
fork, err := repo_service.ForkRepository(ctx.User, forker, repo_service.ForkRepoOptions{
|
||||
fork, err := repo_service.ForkRepository(ctx, ctx.Doer, forker, repo_service.ForkRepoOptions{
|
||||
BaseRepo: repo,
|
||||
Name: name,
|
||||
Description: repo.Description,
|
||||
|
||||
@@ -138,6 +138,11 @@ func TestHook(ctx *context.APIContext) {
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: query
|
||||
// description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
|
||||
// type: string
|
||||
// required: false
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
@@ -163,8 +168,8 @@ func TestHook(ctx *context.APIContext) {
|
||||
Commits: []*api.PayloadCommit{commit},
|
||||
HeadCommit: commit,
|
||||
Repo: convert.ToRepo(ctx.Repo.Repository, perm.AccessModeNone),
|
||||
Pusher: convert.ToUserWithAccessMode(ctx.User, perm.AccessModeNone),
|
||||
Sender: convert.ToUserWithAccessMode(ctx.User, perm.AccessModeNone),
|
||||
Pusher: convert.ToUserWithAccessMode(ctx.Doer, perm.AccessModeNone),
|
||||
Sender: convert.ToUserWithAccessMode(ctx.Doer, perm.AccessModeNone),
|
||||
}); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "PrepareWebhook: ", err)
|
||||
return
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
@@ -110,7 +112,7 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/IssueList"
|
||||
|
||||
before, since, err := utils.GetQueryBeforeSince(ctx)
|
||||
before, since, err := context.GetQueryBeforeSince(ctx.Context)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
|
||||
return
|
||||
@@ -135,7 +137,7 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
// This needs to be a column that is not nil in fixtures or
|
||||
// MySQL will return different results when sorting by null in some cases
|
||||
OrderBy: db.SearchOrderByAlphabetically,
|
||||
Actor: ctx.User,
|
||||
Actor: ctx.Doer,
|
||||
}
|
||||
if ctx.IsSigned {
|
||||
opts.Private = true
|
||||
@@ -161,9 +163,9 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
|
||||
return
|
||||
}
|
||||
team, err := models.GetTeam(opts.OwnerID, ctx.FormString("team"))
|
||||
team, err := organization.GetTeam(opts.OwnerID, ctx.FormString("team"))
|
||||
if err != nil {
|
||||
if models.IsErrTeamNotExist(err) {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.Error(http.StatusBadRequest, "Team not found", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
|
||||
@@ -173,6 +175,7 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
opts.TeamID = team.ID
|
||||
}
|
||||
|
||||
repoCond := models.SearchRepositoryCondition(opts)
|
||||
repoIDs, _, err := models.SearchRepositoryIDs(opts)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "SearchRepositoryByName", err)
|
||||
@@ -233,7 +236,7 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
Page: ctx.FormInt("page"),
|
||||
PageSize: limit,
|
||||
},
|
||||
RepoIDs: repoIDs,
|
||||
RepoCond: repoCond,
|
||||
IsClosed: isClosed,
|
||||
IssueIDs: issueIDs,
|
||||
IncludedLabelNames: includedLabelNames,
|
||||
@@ -245,18 +248,23 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
UpdatedAfterUnix: since,
|
||||
}
|
||||
|
||||
ctxUserID := int64(0)
|
||||
if ctx.IsSigned {
|
||||
ctxUserID = ctx.Doer.ID
|
||||
}
|
||||
|
||||
// Filter for: Created by User, Assigned to User, Mentioning User, Review of User Requested
|
||||
if ctx.FormBool("created") {
|
||||
issuesOpt.PosterID = ctx.User.ID
|
||||
issuesOpt.PosterID = ctxUserID
|
||||
}
|
||||
if ctx.FormBool("assigned") {
|
||||
issuesOpt.AssigneeID = ctx.User.ID
|
||||
issuesOpt.AssigneeID = ctxUserID
|
||||
}
|
||||
if ctx.FormBool("mentioned") {
|
||||
issuesOpt.MentionedID = ctx.User.ID
|
||||
issuesOpt.MentionedID = ctxUserID
|
||||
}
|
||||
if ctx.FormBool("review_requested") {
|
||||
issuesOpt.ReviewRequestedID = ctx.User.ID
|
||||
issuesOpt.ReviewRequestedID = ctxUserID
|
||||
}
|
||||
|
||||
if issues, err = models.Issues(issuesOpt); err != nil {
|
||||
@@ -353,7 +361,7 @@ func ListIssues(ctx *context.APIContext) {
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/IssueList"
|
||||
before, since, err := utils.GetQueryBeforeSince(ctx)
|
||||
before, since, err := context.GetQueryBeforeSince(ctx.Context)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
|
||||
return
|
||||
@@ -399,12 +407,12 @@ func ListIssues(ctx *context.APIContext) {
|
||||
for i := range part {
|
||||
// uses names and fall back to ids
|
||||
// non existent milestones are discarded
|
||||
mile, err := models.GetMilestoneByRepoIDANDName(ctx.Repo.Repository.ID, part[i])
|
||||
mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx.Repo.Repository.ID, part[i])
|
||||
if err == nil {
|
||||
mileIDs = append(mileIDs, mile.ID)
|
||||
continue
|
||||
}
|
||||
if !models.IsErrMilestoneNotExist(err) {
|
||||
if !issues_model.IsErrMilestoneNotExist(err) {
|
||||
ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoIDANDName", err)
|
||||
return
|
||||
}
|
||||
@@ -412,12 +420,12 @@ func ListIssues(ctx *context.APIContext) {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
mile, err = models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, id)
|
||||
mile, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, id)
|
||||
if err == nil {
|
||||
mileIDs = append(mileIDs, mile.ID)
|
||||
continue
|
||||
}
|
||||
if models.IsErrMilestoneNotExist(err) {
|
||||
if issues_model.IsErrMilestoneNotExist(err) {
|
||||
continue
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
|
||||
@@ -455,7 +463,7 @@ func ListIssues(ctx *context.APIContext) {
|
||||
if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
|
||||
issuesOpt := &models.IssuesOptions{
|
||||
ListOptions: listOptions,
|
||||
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsClosed: isClosed,
|
||||
IssueIDs: issueIDs,
|
||||
LabelIDs: labelIDs,
|
||||
@@ -592,8 +600,8 @@ func CreateIssue(ctx *context.APIContext) {
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Repo: ctx.Repo.Repository,
|
||||
Title: form.Title,
|
||||
PosterID: ctx.User.ID,
|
||||
Poster: ctx.User,
|
||||
PosterID: ctx.Doer.ID,
|
||||
Poster: ctx.Doer,
|
||||
Content: form.Body,
|
||||
Ref: form.Ref,
|
||||
DeadlineUnix: deadlineUnix,
|
||||
@@ -646,7 +654,7 @@ func CreateIssue(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if form.Closed {
|
||||
if err := issue_service.ChangeStatus(issue, ctx.User, true); err != nil {
|
||||
if err := issue_service.ChangeStatus(issue, ctx.Doer, true); err != nil {
|
||||
if models.IsErrDependenciesLeft(err) {
|
||||
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
|
||||
return
|
||||
@@ -724,7 +732,7 @@ func EditIssue(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !issue.IsPoster(ctx.User.ID) && !canWrite {
|
||||
if !issue.IsPoster(ctx.Doer.ID) && !canWrite {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -737,7 +745,7 @@ func EditIssue(ctx *context.APIContext) {
|
||||
issue.Content = *form.Body
|
||||
}
|
||||
if form.Ref != nil {
|
||||
err = issue_service.ChangeIssueRef(issue, ctx.User, *form.Ref)
|
||||
err = issue_service.ChangeIssueRef(issue, ctx.Doer, *form.Ref)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateRef", err)
|
||||
return
|
||||
@@ -754,7 +762,7 @@ func EditIssue(ctx *context.APIContext) {
|
||||
deadlineUnix = timeutil.TimeStamp(deadline.Unix())
|
||||
}
|
||||
|
||||
if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.User); err != nil {
|
||||
if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
|
||||
return
|
||||
}
|
||||
@@ -775,7 +783,7 @@ func EditIssue(ctx *context.APIContext) {
|
||||
oneAssignee = *form.Assignee
|
||||
}
|
||||
|
||||
err = issue_service.UpdateAssignees(issue, oneAssignee, form.Assignees, ctx.User)
|
||||
err = issue_service.UpdateAssignees(issue, oneAssignee, form.Assignees, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
|
||||
return
|
||||
@@ -786,7 +794,7 @@ func EditIssue(ctx *context.APIContext) {
|
||||
issue.MilestoneID != *form.Milestone {
|
||||
oldMilestoneID := issue.MilestoneID
|
||||
issue.MilestoneID = *form.Milestone
|
||||
if err = issue_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
|
||||
if err = issue_service.ChangeMilestoneAssign(issue, ctx.Doer, oldMilestoneID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
|
||||
return
|
||||
}
|
||||
@@ -803,7 +811,7 @@ func EditIssue(ctx *context.APIContext) {
|
||||
}
|
||||
issue.IsClosed = api.StateClosed == api.StateType(*form.State)
|
||||
}
|
||||
statusChangeComment, titleChanged, err := models.UpdateIssueByAPI(issue, ctx.User)
|
||||
statusChangeComment, titleChanged, err := models.UpdateIssueByAPI(issue, ctx.Doer)
|
||||
if err != nil {
|
||||
if models.IsErrDependenciesLeft(err) {
|
||||
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
|
||||
@@ -814,11 +822,11 @@ func EditIssue(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if titleChanged {
|
||||
notification.NotifyIssueChangeTitle(ctx.User, issue, oldTitle)
|
||||
notification.NotifyIssueChangeTitle(ctx.Doer, issue, oldTitle)
|
||||
}
|
||||
|
||||
if statusChangeComment != nil {
|
||||
notification.NotifyIssueChangeStatus(ctx.User, issue, statusChangeComment, issue.IsClosed)
|
||||
notification.NotifyIssueChangeStatus(ctx.Doer, issue, statusChangeComment, issue.IsClosed)
|
||||
}
|
||||
|
||||
// Refetch from database to assign some automatic values
|
||||
@@ -872,7 +880,7 @@ func DeleteIssue(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = issue_service.DeleteIssue(ctx.User, ctx.Repo.GitRepo, issue); err != nil {
|
||||
if err = issue_service.DeleteIssue(ctx.Doer, ctx.Repo.GitRepo, issue); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteIssueByID", err)
|
||||
return
|
||||
}
|
||||
@@ -941,7 +949,7 @@ func UpdateIssueDeadline(ctx *context.APIContext) {
|
||||
deadlineUnix = timeutil.TimeStamp(deadline.Unix())
|
||||
}
|
||||
|
||||
if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.User); err != nil {
|
||||
if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
stdCtx "context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
@@ -58,7 +59,7 @@ func ListIssueComments(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/CommentList"
|
||||
|
||||
before, since, err := utils.GetQueryBeforeSince(ctx)
|
||||
before, since, err := context.GetQueryBeforeSince(ctx.Context)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
|
||||
return
|
||||
@@ -150,7 +151,7 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/TimelineList"
|
||||
|
||||
before, since, err := utils.GetQueryBeforeSince(ctx)
|
||||
before, since, err := context.GetQueryBeforeSince(ctx.Context)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
|
||||
return
|
||||
@@ -183,9 +184,9 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
|
||||
|
||||
var apiComments []*api.TimelineComment
|
||||
for _, comment := range comments {
|
||||
if comment.Type != models.CommentTypeCode && isXRefCommentAccessible(ctx.User, comment, issue.RepoID) {
|
||||
if comment.Type != models.CommentTypeCode && isXRefCommentAccessible(ctx, ctx.Doer, comment, issue.RepoID) {
|
||||
comment.Issue = issue
|
||||
apiComments = append(apiComments, convert.ToTimelineComment(comment, ctx.User))
|
||||
apiComments = append(apiComments, convert.ToTimelineComment(comment, ctx.Doer))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,16 +194,16 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
|
||||
ctx.JSON(http.StatusOK, &apiComments)
|
||||
}
|
||||
|
||||
func isXRefCommentAccessible(user *user_model.User, c *models.Comment, issueRepoID int64) bool {
|
||||
func isXRefCommentAccessible(ctx stdCtx.Context, user *user_model.User, c *models.Comment, issueRepoID int64) bool {
|
||||
// Remove comments that the user has no permissions to see
|
||||
if models.CommentTypeIsRef(c.Type) && c.RefRepoID != issueRepoID && c.RefRepoID != 0 {
|
||||
var err error
|
||||
// Set RefRepo for description in template
|
||||
c.RefRepo, err = repo_model.GetRepositoryByID(c.RefRepoID)
|
||||
c.RefRepo, err = repo_model.GetRepositoryByIDCtx(ctx, c.RefRepoID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
perm, err := models.GetUserRepoPermission(c.RefRepo, user)
|
||||
perm, err := models.GetUserRepoPermission(ctx, c.RefRepo, user)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -253,7 +254,7 @@ func ListRepoIssueComments(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/CommentList"
|
||||
|
||||
before, since, err := utils.GetQueryBeforeSince(ctx)
|
||||
before, since, err := context.GetQueryBeforeSince(ctx.Context)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
|
||||
return
|
||||
@@ -347,12 +348,12 @@ func CreateIssueComment(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.User.IsAdmin {
|
||||
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
|
||||
ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked")))
|
||||
return
|
||||
}
|
||||
|
||||
comment, err := comment_service.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Body, nil)
|
||||
comment, err := comment_service.CreateIssueComment(ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
|
||||
return
|
||||
@@ -534,7 +535,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -546,7 +547,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
|
||||
|
||||
oldContent := comment.Content
|
||||
comment.Content = form.Body
|
||||
if err := comment_service.UpdateComment(comment, ctx.User, oldContent); err != nil {
|
||||
if err := comment_service.UpdateComment(comment, ctx.Doer, oldContent); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
|
||||
return
|
||||
}
|
||||
@@ -637,7 +638,7 @@ func deleteIssueComment(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
} else if comment.Type != models.CommentTypeComment {
|
||||
@@ -645,7 +646,7 @@ func deleteIssueComment(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = comment_service.DeleteComment(ctx.User, comment); err != nil {
|
||||
if err = comment_service.DeleteComment(ctx.Doer, comment); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteCommentByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ func AddIssueLabels(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = issue_service.AddLabels(issue, ctx.User, labels); err != nil {
|
||||
if err = issue_service.AddLabels(issue, ctx.Doer, labels); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "AddLabels", err)
|
||||
return
|
||||
}
|
||||
@@ -183,7 +183,7 @@ func DeleteIssueLabel(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := issue_service.RemoveLabel(issue, ctx.User, label); err != nil {
|
||||
if err := issue_service.RemoveLabel(issue, ctx.Doer, label); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteIssueLabel", err)
|
||||
return
|
||||
}
|
||||
@@ -232,7 +232,7 @@ func ReplaceIssueLabels(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := issue_service.ReplaceLabels(issue, ctx.User, labels); err != nil {
|
||||
if err := issue_service.ReplaceLabels(issue, ctx.Doer, labels); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ReplaceLabels", err)
|
||||
return
|
||||
}
|
||||
@@ -291,7 +291,7 @@ func ClearIssueLabels(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := issue_service.ClearLabels(issue, ctx.User); err != nil {
|
||||
if err := issue_service.ClearLabels(issue, ctx.Doer); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ClearLabels", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
@@ -67,12 +68,12 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
reactions, _, err := models.FindCommentReactions(comment)
|
||||
reactions, _, err := issues_model.FindCommentReactions(comment.IssueID, comment.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "FindCommentReactions", err)
|
||||
return
|
||||
}
|
||||
_, err = reactions.LoadUsers(ctx.Repo.Repository)
|
||||
_, err = reactions.LoadUsers(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ReactionList.LoadUsers()", err)
|
||||
return
|
||||
@@ -81,7 +82,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
|
||||
var result []api.Reaction
|
||||
for _, r := range reactions {
|
||||
result = append(result, api.Reaction{
|
||||
User: convert.ToUser(r.User, ctx.User),
|
||||
User: convert.ToUser(r.User, ctx.Doer),
|
||||
Reaction: r.Type,
|
||||
Created: r.CreatedUnix.AsTime(),
|
||||
})
|
||||
@@ -197,13 +198,13 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
|
||||
|
||||
if isCreateType {
|
||||
// PostIssueCommentReaction part
|
||||
reaction, err := models.CreateCommentReaction(ctx.User, comment.Issue, comment, form.Reaction)
|
||||
reaction, err := issues_model.CreateCommentReaction(ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
|
||||
if err != nil {
|
||||
if models.IsErrForbiddenIssueReaction(err) {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
ctx.Error(http.StatusForbidden, err.Error(), err)
|
||||
} else if models.IsErrReactionAlreadyExist(err) {
|
||||
} else if issues_model.IsErrReactionAlreadyExist(err) {
|
||||
ctx.JSON(http.StatusOK, api.Reaction{
|
||||
User: convert.ToUser(ctx.User, ctx.User),
|
||||
User: convert.ToUser(ctx.Doer, ctx.Doer),
|
||||
Reaction: reaction.Type,
|
||||
Created: reaction.CreatedUnix.AsTime(),
|
||||
})
|
||||
@@ -214,13 +215,13 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, api.Reaction{
|
||||
User: convert.ToUser(ctx.User, ctx.User),
|
||||
User: convert.ToUser(ctx.Doer, ctx.Doer),
|
||||
Reaction: reaction.Type,
|
||||
Created: reaction.CreatedUnix.AsTime(),
|
||||
})
|
||||
} else {
|
||||
// DeleteIssueCommentReaction part
|
||||
err = models.DeleteCommentReaction(ctx.User, comment.Issue, comment, form.Reaction)
|
||||
err = issues_model.DeleteCommentReaction(ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteCommentReaction", err)
|
||||
return
|
||||
@@ -285,12 +286,12 @@ func GetIssueReactions(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
reactions, count, err := models.FindIssueReactions(issue, utils.GetListOptions(ctx))
|
||||
reactions, count, err := issues_model.FindIssueReactions(issue.ID, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "FindIssueReactions", err)
|
||||
return
|
||||
}
|
||||
_, err = reactions.LoadUsers(ctx.Repo.Repository)
|
||||
_, err = reactions.LoadUsers(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ReactionList.LoadUsers()", err)
|
||||
return
|
||||
@@ -299,7 +300,7 @@ func GetIssueReactions(ctx *context.APIContext) {
|
||||
var result []api.Reaction
|
||||
for _, r := range reactions {
|
||||
result = append(result, api.Reaction{
|
||||
User: convert.ToUser(r.User, ctx.User),
|
||||
User: convert.ToUser(r.User, ctx.Doer),
|
||||
Reaction: r.Type,
|
||||
Created: r.CreatedUnix.AsTime(),
|
||||
})
|
||||
@@ -407,13 +408,13 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
|
||||
|
||||
if isCreateType {
|
||||
// PostIssueReaction part
|
||||
reaction, err := models.CreateIssueReaction(ctx.User, issue, form.Reaction)
|
||||
reaction, err := issues_model.CreateIssueReaction(ctx.Doer.ID, issue.ID, form.Reaction)
|
||||
if err != nil {
|
||||
if models.IsErrForbiddenIssueReaction(err) {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
ctx.Error(http.StatusForbidden, err.Error(), err)
|
||||
} else if models.IsErrReactionAlreadyExist(err) {
|
||||
} else if issues_model.IsErrReactionAlreadyExist(err) {
|
||||
ctx.JSON(http.StatusOK, api.Reaction{
|
||||
User: convert.ToUser(ctx.User, ctx.User),
|
||||
User: convert.ToUser(ctx.Doer, ctx.Doer),
|
||||
Reaction: reaction.Type,
|
||||
Created: reaction.CreatedUnix.AsTime(),
|
||||
})
|
||||
@@ -424,13 +425,13 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, api.Reaction{
|
||||
User: convert.ToUser(ctx.User, ctx.User),
|
||||
User: convert.ToUser(ctx.Doer, ctx.Doer),
|
||||
Reaction: reaction.Type,
|
||||
Created: reaction.CreatedUnix.AsTime(),
|
||||
})
|
||||
} else {
|
||||
// DeleteIssueReaction part
|
||||
err = models.DeleteIssueReaction(ctx.User, issue, form.Reaction)
|
||||
err = issues_model.DeleteIssueReaction(ctx.Doer.ID, issue.ID, form.Reaction)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteIssueReaction", err)
|
||||
return
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
@@ -56,7 +55,7 @@ func StartIssueStopwatch(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.CreateIssueStopwatch(db.DefaultContext, ctx.User, issue); err != nil {
|
||||
if err := models.CreateIssueStopwatch(ctx, ctx.Doer, issue); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateOrStopIssueStopwatch", err)
|
||||
return
|
||||
}
|
||||
@@ -105,7 +104,7 @@ func StopIssueStopwatch(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.FinishIssueStopwatch(db.DefaultContext, ctx.User, issue); err != nil {
|
||||
if err := models.FinishIssueStopwatch(ctx, ctx.Doer, issue); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateOrStopIssueStopwatch", err)
|
||||
return
|
||||
}
|
||||
@@ -154,7 +153,7 @@ func DeleteIssueStopwatch(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.CancelStopwatch(ctx.User, issue); err != nil {
|
||||
if err := models.CancelStopwatch(ctx.Doer, issue); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CancelStopwatch", err)
|
||||
return
|
||||
}
|
||||
@@ -179,12 +178,12 @@ func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*models.I
|
||||
return nil, errors.New("Unable to write to PRs")
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanUseTimetracker(issue, ctx.User) {
|
||||
if !ctx.Repo.CanUseTimetracker(issue, ctx.Doer) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return nil, errors.New("Cannot use time tracker")
|
||||
}
|
||||
|
||||
if models.StopwatchExists(ctx.User.ID, issue.ID) != shouldExist {
|
||||
if models.StopwatchExists(ctx.Doer.ID, issue.ID) != shouldExist {
|
||||
if shouldExist {
|
||||
ctx.Error(http.StatusConflict, "StopwatchExists", "cannot stop/cancel a non existent stopwatch")
|
||||
err = errors.New("cannot stop/cancel a non existent stopwatch")
|
||||
@@ -220,13 +219,13 @@ func GetStopwatches(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/StopWatchList"
|
||||
|
||||
sws, err := models.GetUserStopwatches(ctx.User.ID, utils.GetListOptions(ctx))
|
||||
sws, err := models.GetUserStopwatches(ctx.Doer.ID, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserStopwatches", err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := models.CountUserStopwatches(ctx.User.ID)
|
||||
count, err := models.CountUserStopwatches(ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
|
||||
@@ -128,8 +128,8 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) {
|
||||
}
|
||||
|
||||
// only admin and user for itself can change subscription
|
||||
if user.ID != ctx.User.ID && !ctx.User.IsAdmin {
|
||||
ctx.Error(http.StatusForbidden, "User", fmt.Errorf("%s is not permitted to change subscriptions for %s", ctx.User.Name, user.Name))
|
||||
if user.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
|
||||
ctx.Error(http.StatusForbidden, "User", fmt.Errorf("%s is not permitted to change subscriptions for %s", ctx.Doer.Name, user.Name))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ func CheckIssueSubscription(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
watching, err := models.CheckIssueWatch(ctx.User, issue)
|
||||
watching, err := models.CheckIssueWatch(ctx.Doer, issue)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
@@ -281,7 +281,7 @@ func GetIssueSubscribers(ctx *context.APIContext) {
|
||||
}
|
||||
apiUsers := make([]*api.User, 0, len(users))
|
||||
for _, v := range users {
|
||||
apiUsers = append(apiUsers, convert.ToUser(v, ctx.User))
|
||||
apiUsers = append(apiUsers, convert.ToUser(v, ctx.Doer))
|
||||
}
|
||||
|
||||
count, err := models.CountIssueWatchers(issue.ID)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
@@ -103,18 +104,18 @@ func ListTrackedTimes(ctx *context.APIContext) {
|
||||
opts.UserID = user.ID
|
||||
}
|
||||
|
||||
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil {
|
||||
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
|
||||
return
|
||||
}
|
||||
|
||||
cantSetUser := !ctx.User.IsAdmin &&
|
||||
opts.UserID != ctx.User.ID &&
|
||||
cantSetUser := !ctx.Doer.IsAdmin &&
|
||||
opts.UserID != ctx.Doer.ID &&
|
||||
!ctx.IsUserRepoWriter([]unit.Type{unit.TypeIssues})
|
||||
|
||||
if cantSetUser {
|
||||
if opts.UserID == 0 {
|
||||
opts.UserID = ctx.User.ID
|
||||
opts.UserID = ctx.Doer.ID
|
||||
} else {
|
||||
ctx.Error(http.StatusForbidden, "", fmt.Errorf("query by user not allowed; not enough rights"))
|
||||
return
|
||||
@@ -189,7 +190,7 @@ func AddTime(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanUseTimetracker(issue, ctx.User) {
|
||||
if !ctx.Repo.CanUseTimetracker(issue, ctx.Doer) {
|
||||
if !ctx.Repo.Repository.IsTimetrackerEnabled() {
|
||||
ctx.Error(http.StatusBadRequest, "", "time tracking disabled")
|
||||
return
|
||||
@@ -198,9 +199,9 @@ func AddTime(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.User
|
||||
user := ctx.Doer
|
||||
if form.User != "" {
|
||||
if (ctx.IsUserRepoAdmin() && ctx.User.Name != form.User) || ctx.User.IsAdmin {
|
||||
if (ctx.IsUserRepoAdmin() && ctx.Doer.Name != form.User) || ctx.Doer.IsAdmin {
|
||||
// allow only RepoAdmin, Admin and User to add time
|
||||
user, err = user_model.GetUserByName(form.User)
|
||||
if err != nil {
|
||||
@@ -270,7 +271,7 @@ func ResetIssueTime(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanUseTimetracker(issue, ctx.User) {
|
||||
if !ctx.Repo.CanUseTimetracker(issue, ctx.Doer) {
|
||||
if !ctx.Repo.Repository.IsTimetrackerEnabled() {
|
||||
ctx.JSON(http.StatusBadRequest, struct{ Message string }{Message: "time tracking disabled"})
|
||||
return
|
||||
@@ -279,16 +280,16 @@ func ResetIssueTime(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
err = models.DeleteIssueUserTimes(issue, ctx.User)
|
||||
err = models.DeleteIssueUserTimes(issue, ctx.Doer)
|
||||
if err != nil {
|
||||
if models.IsErrNotExist(err) {
|
||||
if db.IsErrNotExist(err) {
|
||||
ctx.Error(http.StatusNotFound, "DeleteIssueUserTimes", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteIssueUserTimes", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.Status(204)
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// DeleteTime delete a specific time by id
|
||||
@@ -341,7 +342,7 @@ func DeleteTime(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanUseTimetracker(issue, ctx.User) {
|
||||
if !ctx.Repo.CanUseTimetracker(issue, ctx.Doer) {
|
||||
if !ctx.Repo.Repository.IsTimetrackerEnabled() {
|
||||
ctx.JSON(http.StatusBadRequest, struct{ Message string }{Message: "time tracking disabled"})
|
||||
return
|
||||
@@ -352,7 +353,7 @@ func DeleteTime(ctx *context.APIContext) {
|
||||
|
||||
time, err := models.GetTrackedTimeByID(ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if models.IsErrNotExist(err) {
|
||||
if db.IsErrNotExist(err) {
|
||||
ctx.NotFound(err)
|
||||
return
|
||||
}
|
||||
@@ -364,7 +365,7 @@ func DeleteTime(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.User.IsAdmin && time.UserID != ctx.User.ID {
|
||||
if !ctx.Doer.IsAdmin && time.UserID != ctx.Doer.ID {
|
||||
// Only Admin and User itself can delete their time
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
@@ -428,7 +429,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsUserRepoAdmin() && !ctx.User.IsAdmin && ctx.User.ID != user.ID {
|
||||
if !ctx.IsUserRepoAdmin() && !ctx.Doer.IsAdmin && ctx.Doer.ID != user.ID {
|
||||
ctx.Error(http.StatusForbidden, "", fmt.Errorf("query by user not allowed; not enough rights"))
|
||||
return
|
||||
}
|
||||
@@ -522,18 +523,18 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
var err error
|
||||
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil {
|
||||
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
|
||||
return
|
||||
}
|
||||
|
||||
cantSetUser := !ctx.User.IsAdmin &&
|
||||
opts.UserID != ctx.User.ID &&
|
||||
cantSetUser := !ctx.Doer.IsAdmin &&
|
||||
opts.UserID != ctx.Doer.ID &&
|
||||
!ctx.IsUserRepoWriter([]unit.Type{unit.TypeIssues})
|
||||
|
||||
if cantSetUser {
|
||||
if opts.UserID == 0 {
|
||||
opts.UserID = ctx.User.ID
|
||||
opts.UserID = ctx.Doer.ID
|
||||
} else {
|
||||
ctx.Error(http.StatusForbidden, "", fmt.Errorf("query by user not allowed; not enough rights"))
|
||||
return
|
||||
@@ -593,11 +594,11 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
|
||||
|
||||
opts := &models.FindTrackedTimesOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
UserID: ctx.User.ID,
|
||||
UserID: ctx.Doer.ID,
|
||||
}
|
||||
|
||||
var err error
|
||||
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil {
|
||||
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func ListDeployKeys(ctx *context.APIContext) {
|
||||
Fingerprint: ctx.FormString("fingerprint"),
|
||||
}
|
||||
|
||||
keys, err := asymkey_model.ListDeployKeys(db.DefaultContext, opts)
|
||||
keys, err := asymkey_model.ListDeployKeys(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
@@ -107,7 +107,7 @@ func ListDeployKeys(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
apiKeys[i] = convert.ToDeployKey(apiLink, keys[i])
|
||||
if ctx.User.IsAdmin || ((ctx.Repo.Repository.ID == keys[i].RepoID) && (ctx.User.ID == ctx.Repo.Owner.ID)) {
|
||||
if ctx.Doer.IsAdmin || ((ctx.Repo.Repository.ID == keys[i].RepoID) && (ctx.Doer.ID == ctx.Repo.Owner.ID)) {
|
||||
apiKeys[i], _ = appendPrivateInformation(apiKeys[i], keys[i], ctx.Repo.Repository)
|
||||
}
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func GetDeployKey(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/DeployKey"
|
||||
|
||||
key, err := asymkey_model.GetDeployKeyByID(db.DefaultContext, ctx.ParamsInt64(":id"))
|
||||
key, err := asymkey_model.GetDeployKeyByID(ctx, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if asymkey_model.IsErrDeployKeyNotExist(err) {
|
||||
ctx.NotFound()
|
||||
@@ -161,7 +161,7 @@ func GetDeployKey(ctx *context.APIContext) {
|
||||
|
||||
apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
|
||||
apiKey := convert.ToDeployKey(apiLink, key)
|
||||
if ctx.User.IsAdmin || ((ctx.Repo.Repository.ID == key.RepoID) && (ctx.User.ID == ctx.Repo.Owner.ID)) {
|
||||
if ctx.Doer.IsAdmin || ((ctx.Repo.Repository.ID == key.RepoID) && (ctx.Doer.ID == ctx.Repo.Owner.ID)) {
|
||||
apiKey, _ = appendPrivateInformation(apiKey, key, ctx.Repo.Repository)
|
||||
}
|
||||
ctx.JSON(http.StatusOK, apiKey)
|
||||
@@ -270,7 +270,7 @@ func DeleteDeploykey(ctx *context.APIContext) {
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
|
||||
if err := asymkey_service.DeleteDeployKey(ctx.User, ctx.ParamsInt64(":id")); err != nil {
|
||||
if err := asymkey_service.DeleteDeployKey(ctx.Doer, ctx.ParamsInt64(":id")); err != nil {
|
||||
if asymkey_model.IsErrKeyAccessDenied(err) {
|
||||
ctx.Error(http.StatusForbidden, "", "You do not have access to this key")
|
||||
} else {
|
||||
|
||||
@@ -161,7 +161,7 @@ func CreateLabel(ctx *context.APIContext) {
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Description: form.Description,
|
||||
}
|
||||
if err := models.NewLabel(label); err != nil {
|
||||
if err := models.NewLabel(ctx, label); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "NewLabel", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -76,9 +76,7 @@ func GetLanguages(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
resp := make(languageResponse, len(langs))
|
||||
for i, v := range langs {
|
||||
resp[i] = v
|
||||
}
|
||||
copy(resp, langs)
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,15 @@ import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
webhook_service "code.gitea.io/gitea/services/webhook"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m, filepath.Join("..", "..", "..", ".."))
|
||||
setting.LoadForTest()
|
||||
setting.NewQueueService()
|
||||
unittest.MainTest(m, &unittest.TestOptions{
|
||||
GiteaRootPath: filepath.Join("..", "..", "..", ".."),
|
||||
SetUp: webhook_service.Init,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@@ -51,6 +52,8 @@ func Migrate(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/Repository"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "409":
|
||||
// description: The repository with the same name already exists.
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
@@ -66,7 +69,7 @@ func Migrate(ctx *context.APIContext) {
|
||||
} else if form.RepoOwnerID != 0 {
|
||||
repoOwner, err = user_model.GetUserByID(form.RepoOwnerID)
|
||||
} else {
|
||||
repoOwner = ctx.User
|
||||
repoOwner = ctx.Doer
|
||||
}
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
@@ -82,15 +85,15 @@ func Migrate(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.User.IsAdmin {
|
||||
if !repoOwner.IsOrganization() && ctx.User.ID != repoOwner.ID {
|
||||
if !ctx.Doer.IsAdmin {
|
||||
if !repoOwner.IsOrganization() && ctx.Doer.ID != repoOwner.ID {
|
||||
ctx.Error(http.StatusForbidden, "", "Given user is not an organization.")
|
||||
return
|
||||
}
|
||||
|
||||
if repoOwner.IsOrganization() {
|
||||
// Check ownership of organization.
|
||||
isOwner, err := models.OrgFromUser(repoOwner).IsOwnedBy(ctx.User.ID)
|
||||
isOwner, err := organization.OrgFromUser(repoOwner).IsOwnedBy(ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOwnedBy", err)
|
||||
return
|
||||
@@ -103,7 +106,7 @@ func Migrate(ctx *context.APIContext) {
|
||||
|
||||
remoteAddr, err := forms.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword)
|
||||
if err == nil {
|
||||
err = migrations.IsMigrateURLAllowed(remoteAddr, ctx.User)
|
||||
err = migrations.IsMigrateURLAllowed(remoteAddr, ctx.Doer)
|
||||
}
|
||||
if err != nil {
|
||||
handleRemoteAddrError(ctx, err)
|
||||
@@ -130,7 +133,7 @@ func Migrate(ctx *context.APIContext) {
|
||||
ctx.Error(http.StatusInternalServerError, "", ctx.Tr("repo.migrate.invalid_lfs_endpoint"))
|
||||
return
|
||||
}
|
||||
err = migrations.IsMigrateURLAllowed(ep.String(), ctx.User)
|
||||
err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer)
|
||||
if err != nil {
|
||||
handleRemoteAddrError(ctx, err)
|
||||
return
|
||||
@@ -167,7 +170,7 @@ func Migrate(ctx *context.APIContext) {
|
||||
opts.Releases = false
|
||||
}
|
||||
|
||||
repo, err := repo_module.CreateRepository(ctx.User, repoOwner, models.CreateRepoOptions{
|
||||
repo, err := repo_module.CreateRepository(ctx.Doer, repoOwner, models.CreateRepoOptions{
|
||||
Name: opts.RepoName,
|
||||
Description: opts.Description,
|
||||
OriginalURL: form.CloneAddr,
|
||||
@@ -192,18 +195,18 @@ func Migrate(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
notification.NotifyMigrateRepository(ctx.User, repoOwner, repo)
|
||||
notification.NotifyMigrateRepository(ctx.Doer, repoOwner, repo)
|
||||
return
|
||||
}
|
||||
|
||||
if repo != nil {
|
||||
if errDelete := models.DeleteRepository(ctx.User, repoOwner.ID, repo.ID); errDelete != nil {
|
||||
if errDelete := models.DeleteRepository(ctx.Doer, repoOwner.ID, repo.ID); errDelete != nil {
|
||||
log.Error("DeleteRepository: %v", errDelete)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if repo, err = migrations.MigrateRepository(graceful.GetManager().HammerContext(), ctx.User, repoOwner.Name, opts, nil); err != nil {
|
||||
if repo, err = migrations.MigrateRepository(graceful.GetManager().HammerContext(), ctx.Doer, repoOwner.Name, opts, nil); err != nil {
|
||||
handleMigrateError(ctx, repoOwner, remoteAddr, err)
|
||||
return
|
||||
}
|
||||
@@ -235,7 +238,7 @@ func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, rem
|
||||
case base.IsErrNotSupported(err):
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||
default:
|
||||
err = util.NewStringURLSanitizedError(err, remoteAddr, true)
|
||||
err = util.SanitizeErrorCredentialURLs(err)
|
||||
if strings.Contains(err.Error(), "Authentication failed") ||
|
||||
strings.Contains(err.Error(), "Bad credentials") ||
|
||||
strings.Contains(err.Error(), "could not read Username") {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
@@ -57,7 +57,7 @@ func ListMilestones(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/MilestoneList"
|
||||
|
||||
milestones, total, err := models.GetMilestones(models.GetMilestonesOption{
|
||||
milestones, total, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
State: api.StateType(ctx.FormString("state")),
|
||||
@@ -146,7 +146,7 @@ func CreateMilestone(ctx *context.APIContext) {
|
||||
form.Deadline = &defaultDeadline
|
||||
}
|
||||
|
||||
milestone := &models.Milestone{
|
||||
milestone := &issues_model.Milestone{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Name: form.Title,
|
||||
Content: form.Description,
|
||||
@@ -158,7 +158,7 @@ func CreateMilestone(ctx *context.APIContext) {
|
||||
milestone.ClosedDateUnix = timeutil.TimeStampNow()
|
||||
}
|
||||
|
||||
if err := models.NewMilestone(milestone); err != nil {
|
||||
if err := issues_model.NewMilestone(milestone); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "NewMilestone", err)
|
||||
return
|
||||
}
|
||||
@@ -218,7 +218,7 @@ func EditMilestone(ctx *context.APIContext) {
|
||||
milestone.IsClosed = *form.State == string(api.StateClosed)
|
||||
}
|
||||
|
||||
if err := models.UpdateMilestone(milestone, oldIsClosed); err != nil {
|
||||
if err := issues_model.UpdateMilestone(milestone, oldIsClosed); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateMilestone", err)
|
||||
return
|
||||
}
|
||||
@@ -255,7 +255,7 @@ func DeleteMilestone(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.DeleteMilestoneByRepoID(ctx.Repo.Repository.ID, m.ID); err != nil {
|
||||
if err := issues_model.DeleteMilestoneByRepoID(ctx.Repo.Repository.ID, m.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteMilestoneByRepoID", err)
|
||||
return
|
||||
}
|
||||
@@ -263,23 +263,23 @@ func DeleteMilestone(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
// getMilestoneByIDOrName get milestone by ID and if not available by name
|
||||
func getMilestoneByIDOrName(ctx *context.APIContext) *models.Milestone {
|
||||
func getMilestoneByIDOrName(ctx *context.APIContext) *issues_model.Milestone {
|
||||
mile := ctx.Params(":id")
|
||||
mileID, _ := strconv.ParseInt(mile, 0, 64)
|
||||
|
||||
if mileID != 0 {
|
||||
milestone, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, mileID)
|
||||
milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, mileID)
|
||||
if err == nil {
|
||||
return milestone
|
||||
} else if !models.IsErrMilestoneNotExist(err) {
|
||||
} else if !issues_model.IsErrMilestoneNotExist(err) {
|
||||
ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
milestone, err := models.GetMilestoneByRepoIDANDName(ctx.Repo.Repository.ID, mile)
|
||||
milestone, err := issues_model.GetMilestoneByRepoIDANDName(ctx.Repo.Repository.ID, mile)
|
||||
if err != nil {
|
||||
if models.IsErrMilestoneNotExist(err) {
|
||||
if issues_model.IsErrMilestoneNotExist(err) {
|
||||
ctx.NotFound()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -48,6 +50,15 @@ func MirrorSync(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := repo_model.GetMirrorByRepoID(repo.ID); err != nil {
|
||||
if errors.Is(err, repo_model.ErrMirrorNotExist) {
|
||||
ctx.Error(http.StatusBadRequest, "MirrorSync", "Repository is not a mirror")
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "MirrorSync", err)
|
||||
return
|
||||
}
|
||||
|
||||
mirror_service.StartToMirror(repo.ID)
|
||||
|
||||
ctx.Status(http.StatusOK)
|
||||
|
||||
@@ -55,15 +55,13 @@ func GetNote(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
func getNote(ctx *context.APIContext, identifier string) {
|
||||
gitRepo, err := git.OpenRepositoryCtx(ctx, ctx.Repo.Repository.RepoPath())
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
ctx.InternalServerError(fmt.Errorf("no open git repo"))
|
||||
return
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
var note git.Note
|
||||
err = git.GetNote(ctx, gitRepo, identifier, ¬e)
|
||||
if err != nil {
|
||||
if err := git.GetNote(ctx, ctx.Repo.GitRepo, identifier, ¬e); err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.NotFound(identifier)
|
||||
return
|
||||
@@ -72,7 +70,7 @@ func getNote(ctx *context.APIContext, identifier string) {
|
||||
return
|
||||
}
|
||||
|
||||
cmt, err := convert.ToCommit(ctx.Repo.Repository, gitRepo, note.Commit, nil)
|
||||
cmt, err := convert.ToCommit(ctx.Repo.Repository, ctx.Repo.GitRepo, note.Commit, nil)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ToCommit", err)
|
||||
return
|
||||
|
||||
@@ -77,15 +77,15 @@ func ApplyDiffPatch(ctx *context.APIContext) {
|
||||
opts.Message = "apply-patch"
|
||||
}
|
||||
|
||||
if !canWriteFiles(ctx.Repo) {
|
||||
if !canWriteFiles(ctx, apiOpts.BranchName) {
|
||||
ctx.Error(http.StatusInternalServerError, "ApplyPatch", models.ErrUserDoesNotHaveAccessToRepo{
|
||||
UserID: ctx.User.ID,
|
||||
UserID: ctx.Doer.ID,
|
||||
RepoName: ctx.Repo.Repository.LowerName,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.User, opts)
|
||||
fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts)
|
||||
if err != nil {
|
||||
if models.IsErrUserCannotCommit(err) || models.IsErrFilePathProtected(err) {
|
||||
ctx.Error(http.StatusForbidden, "Access", err)
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
pull_model "code.gitea.io/gitea/models/pull"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@@ -27,6 +29,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/automerge"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
@@ -110,15 +113,15 @@ func ListPullRequests(ctx *context.APIContext) {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
if err = prs[i].LoadBaseRepo(); err != nil {
|
||||
if err = prs[i].LoadBaseRepoCtx(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
|
||||
return
|
||||
}
|
||||
if err = prs[i].LoadHeadRepo(); err != nil {
|
||||
if err = prs[i].LoadHeadRepoCtx(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
|
||||
return
|
||||
}
|
||||
apiPrs[i] = convert.ToAPIPullRequest(ctx, prs[i], ctx.User)
|
||||
apiPrs[i] = convert.ToAPIPullRequest(ctx, prs[i], ctx.Doer)
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
|
||||
@@ -166,15 +169,15 @@ func GetPullRequest(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = pr.LoadBaseRepo(); err != nil {
|
||||
if err = pr.LoadBaseRepoCtx(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
|
||||
return
|
||||
}
|
||||
if err = pr.LoadHeadRepo(); err != nil {
|
||||
if err = pr.LoadHeadRepoCtx(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.User))
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
|
||||
}
|
||||
|
||||
// DownloadPullDiffOrPatch render a pull's raw diff or patch
|
||||
@@ -342,9 +345,9 @@ func CreatePullRequest(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if form.Milestone > 0 {
|
||||
milestone, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, form.Milestone)
|
||||
milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone)
|
||||
if err != nil {
|
||||
if models.IsErrMilestoneNotExist(err) {
|
||||
if issues_model.IsErrMilestoneNotExist(err) {
|
||||
ctx.NotFound()
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
|
||||
@@ -363,8 +366,8 @@ func CreatePullRequest(ctx *context.APIContext) {
|
||||
prIssue := &models.Issue{
|
||||
RepoID: repo.ID,
|
||||
Title: form.Title,
|
||||
PosterID: ctx.User.ID,
|
||||
Poster: ctx.User,
|
||||
PosterID: ctx.Doer.ID,
|
||||
Poster: ctx.Doer,
|
||||
MilestoneID: milestoneID,
|
||||
IsPull: true,
|
||||
Content: form.Body,
|
||||
@@ -420,7 +423,7 @@ func CreatePullRequest(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIPullRequest(ctx, pr, ctx.User))
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
|
||||
}
|
||||
|
||||
// EditPullRequest does what it says
|
||||
@@ -484,7 +487,7 @@ func EditPullRequest(ctx *context.APIContext) {
|
||||
issue := pr.Issue
|
||||
issue.Repo = ctx.Repo.Repository
|
||||
|
||||
if !issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWrite(unit.TypePullRequests) {
|
||||
if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWrite(unit.TypePullRequests) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -506,7 +509,7 @@ func EditPullRequest(ctx *context.APIContext) {
|
||||
deadlineUnix = timeutil.TimeStamp(deadline.Unix())
|
||||
}
|
||||
|
||||
if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.User); err != nil {
|
||||
if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
|
||||
return
|
||||
}
|
||||
@@ -522,7 +525,7 @@ func EditPullRequest(ctx *context.APIContext) {
|
||||
// Send an empty array ([]) to clear all assignees from the Issue.
|
||||
|
||||
if ctx.Repo.CanWrite(unit.TypePullRequests) && (form.Assignees != nil || len(form.Assignee) > 0) {
|
||||
err = issue_service.UpdateAssignees(issue, form.Assignee, form.Assignees, ctx.User)
|
||||
err = issue_service.UpdateAssignees(issue, form.Assignee, form.Assignees, ctx.Doer)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
|
||||
@@ -537,7 +540,7 @@ func EditPullRequest(ctx *context.APIContext) {
|
||||
issue.MilestoneID != form.Milestone {
|
||||
oldMilestoneID := issue.MilestoneID
|
||||
issue.MilestoneID = form.Milestone
|
||||
if err = issue_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
|
||||
if err = issue_service.ChangeMilestoneAssign(issue, ctx.Doer, oldMilestoneID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
|
||||
return
|
||||
}
|
||||
@@ -560,7 +563,7 @@ func EditPullRequest(ctx *context.APIContext) {
|
||||
labels = append(labels, orgLabels...)
|
||||
}
|
||||
|
||||
if err = issue.ReplaceLabels(labels, ctx.User); err != nil {
|
||||
if err = models.ReplaceIssueLabels(issue, labels, ctx.Doer); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ReplaceLabelsError", err)
|
||||
return
|
||||
}
|
||||
@@ -573,7 +576,7 @@ func EditPullRequest(ctx *context.APIContext) {
|
||||
}
|
||||
issue.IsClosed = api.StateClosed == api.StateType(*form.State)
|
||||
}
|
||||
statusChangeComment, titleChanged, err := models.UpdateIssueByAPI(issue, ctx.User)
|
||||
statusChangeComment, titleChanged, err := models.UpdateIssueByAPI(issue, ctx.Doer)
|
||||
if err != nil {
|
||||
if models.IsErrDependenciesLeft(err) {
|
||||
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies")
|
||||
@@ -584,11 +587,11 @@ func EditPullRequest(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if titleChanged {
|
||||
notification.NotifyIssueChangeTitle(ctx.User, issue, oldTitle)
|
||||
notification.NotifyIssueChangeTitle(ctx.Doer, issue, oldTitle)
|
||||
}
|
||||
|
||||
if statusChangeComment != nil {
|
||||
notification.NotifyIssueChangeStatus(ctx.User, issue, statusChangeComment, issue.IsClosed)
|
||||
notification.NotifyIssueChangeStatus(ctx.Doer, issue, statusChangeComment, issue.IsClosed)
|
||||
}
|
||||
|
||||
// change pull target branch
|
||||
@@ -597,7 +600,7 @@ func EditPullRequest(ctx *context.APIContext) {
|
||||
ctx.Error(http.StatusNotFound, "NewBaseBranchNotExist", fmt.Errorf("new base '%s' not exist", form.Base))
|
||||
return
|
||||
}
|
||||
if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.User, form.Base); err != nil {
|
||||
if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, form.Base); err != nil {
|
||||
if models.IsErrPullRequestAlreadyExists(err) {
|
||||
ctx.Error(http.StatusConflict, "IsErrPullRequestAlreadyExists", err)
|
||||
return
|
||||
@@ -612,7 +615,19 @@ func EditPullRequest(ctx *context.APIContext) {
|
||||
}
|
||||
return
|
||||
}
|
||||
notification.NotifyPullRequestChangeTargetBranch(ctx.User, pr, form.Base)
|
||||
notification.NotifyPullRequestChangeTargetBranch(ctx.Doer, pr, form.Base)
|
||||
}
|
||||
|
||||
// update allow edits
|
||||
if form.AllowMaintainerEdit != nil {
|
||||
if err := pull_service.SetAllowEdits(ctx, ctx.Doer, pr, *form.AllowMaintainerEdit); err != nil {
|
||||
if errors.Is(pull_service.ErrUserHasNoPermissionForAction, err) {
|
||||
ctx.Error(http.StatusForbidden, "SetAllowEdits", fmt.Sprintf("SetAllowEdits: %s", err))
|
||||
return
|
||||
}
|
||||
ctx.ServerError("SetAllowEdits", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Refetch from database
|
||||
@@ -627,7 +642,7 @@ func EditPullRequest(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
// TODO this should be 200, not 201
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIPullRequest(ctx, pr, ctx.User))
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
|
||||
}
|
||||
|
||||
// IsPullRequestMerged checks if a PR exists given an index
|
||||
@@ -713,7 +728,8 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
form := web.GetForm(ctx).(*forms.MergePullRequestForm)
|
||||
pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
||||
|
||||
pr, err := models.GetPullRequestByIndexCtx(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
||||
if err != nil {
|
||||
if models.IsErrPullRequestNotExist(err) {
|
||||
ctx.NotFound("GetPullRequestByIndex", err)
|
||||
@@ -723,13 +739,12 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = pr.LoadHeadRepo(); err != nil {
|
||||
if err := pr.LoadHeadRepoCtx(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = pr.LoadIssue()
|
||||
if err != nil {
|
||||
if err := pr.LoadIssueCtx(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
||||
return
|
||||
}
|
||||
@@ -737,35 +752,40 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||
|
||||
if ctx.IsSigned {
|
||||
// Update issue-user.
|
||||
if err = pr.Issue.ReadBy(ctx.User.ID); err != nil {
|
||||
if err = pr.Issue.ReadBy(ctx, ctx.Doer.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ReadBy", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if pr.Issue.IsClosed {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
manuallMerge := repo_model.MergeStyle(form.Do) == repo_model.MergeStyleManuallyMerged
|
||||
force := form.ForceMerge != nil && *form.ForceMerge
|
||||
|
||||
allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, ctx.Repo.Permission, ctx.User)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsUSerAllowedToMerge", err)
|
||||
return
|
||||
}
|
||||
if !allowedMerge {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "Merge", "User not allowed to merge PR")
|
||||
return
|
||||
}
|
||||
|
||||
if pr.HasMerged {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "PR already merged", "")
|
||||
// start with merging by checking
|
||||
if err := pull_service.CheckPullMergable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, manuallMerge, force); err != nil {
|
||||
if errors.Is(err, pull_service.ErrIsClosed) {
|
||||
ctx.NotFound()
|
||||
} else if errors.Is(err, pull_service.ErrUserNotAllowedToMerge) {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "Merge", "User not allowed to merge PR")
|
||||
} else if errors.Is(err, pull_service.ErrHasMerged) {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "PR already merged", "")
|
||||
} else if errors.Is(err, pull_service.ErrIsWorkInProgress) {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "PR is a work in progress", "Work in progress PRs cannot be merged")
|
||||
} else if errors.Is(err, pull_service.ErrNotMergableState) {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "PR not in mergeable state", "Please try again later")
|
||||
} else if models.IsErrDisallowedToMerge(err) {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "PR is not ready to be merged", err)
|
||||
} else if asymkey_service.IsErrWontSign(err) {
|
||||
ctx.Error(http.StatusMethodNotAllowed, fmt.Sprintf("Protected branch %s requires signed commits but this merge would not be signed", pr.BaseBranch), err)
|
||||
} else {
|
||||
ctx.InternalServerError(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// handle manually-merged mark
|
||||
if repo_model.MergeStyle(form.Do) == repo_model.MergeStyleManuallyMerged {
|
||||
if err = pull_service.MergedManually(pr, ctx.User, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
|
||||
if manuallMerge {
|
||||
if err := pull_service.MergedManually(pr, ctx.Doer, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
|
||||
if models.IsErrInvalidMergeStyle(err) {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
|
||||
return
|
||||
@@ -781,54 +801,16 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !pr.CanAutoMerge() {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "PR not in mergeable state", "Please try again later")
|
||||
return
|
||||
}
|
||||
|
||||
if pr.IsWorkInProgress() {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "PR is a work in progress", "Work in progress PRs cannot be merged")
|
||||
return
|
||||
}
|
||||
|
||||
if err := pull_service.CheckPRReadyToMerge(ctx, pr, false); err != nil {
|
||||
if !models.IsErrNotAllowedToMerge(err) {
|
||||
ctx.Error(http.StatusInternalServerError, "CheckPRReadyToMerge", err)
|
||||
return
|
||||
}
|
||||
if form.ForceMerge != nil && *form.ForceMerge {
|
||||
if isRepoAdmin, err := models.IsUserRepoAdmin(pr.BaseRepo, ctx.User); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsUserRepoAdmin", err)
|
||||
return
|
||||
} else if !isRepoAdmin {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "Merge", "Only repository admin can merge if not all checks are ok (force merge)")
|
||||
}
|
||||
} else {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "PR is not ready to be merged", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := pull_service.IsSignedIfRequired(ctx, pr, ctx.User); err != nil {
|
||||
if !asymkey_service.IsErrWontSign(err) {
|
||||
ctx.Error(http.StatusInternalServerError, "IsSignedIfRequired", err)
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusMethodNotAllowed, fmt.Sprintf("Protected branch %s requires signed commits but this merge would not be signed", pr.BaseBranch), err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(form.Do) == 0 {
|
||||
form.Do = string(repo_model.MergeStyleMerge)
|
||||
}
|
||||
|
||||
message := strings.TrimSpace(form.MergeTitleField)
|
||||
if len(message) == 0 {
|
||||
if repo_model.MergeStyle(form.Do) == repo_model.MergeStyleMerge {
|
||||
message = pr.GetDefaultMergeMessage()
|
||||
}
|
||||
if repo_model.MergeStyle(form.Do) == repo_model.MergeStyleSquash {
|
||||
message = pr.GetDefaultSquashMessage()
|
||||
message, err = pull_service.GetDefaultMergeMessage(ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do))
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetDefaultMergeMessage", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -837,10 +819,25 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||
message += "\n\n" + form.MergeMessageField
|
||||
}
|
||||
|
||||
if err := pull_service.Merge(ctx, pr, ctx.User, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message); err != nil {
|
||||
if form.MergeWhenChecksSucceed {
|
||||
scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message)
|
||||
if err != nil {
|
||||
if pull_model.IsErrAlreadyScheduledToAutoMerge(err) {
|
||||
ctx.Error(http.StatusConflict, "ScheduleAutoMerge", err)
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "ScheduleAutoMerge", err)
|
||||
return
|
||||
} else if scheduled {
|
||||
// nothing more to do ...
|
||||
ctx.Status(http.StatusCreated)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message); err != nil {
|
||||
if models.IsErrInvalidMergeStyle(err) {
|
||||
ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
|
||||
return
|
||||
} else if models.IsErrMergeConflicts(err) {
|
||||
conflictError := err.(models.ErrMergeConflicts)
|
||||
ctx.JSON(http.StatusConflict, conflictError)
|
||||
@@ -852,28 +849,25 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||
ctx.JSON(http.StatusConflict, conflictError)
|
||||
} else if git.IsErrPushOutOfDate(err) {
|
||||
ctx.Error(http.StatusConflict, "Merge", "merge push out of date")
|
||||
return
|
||||
} else if models.IsErrSHADoesNotMatch(err) {
|
||||
ctx.Error(http.StatusConflict, "Merge", "head out of date")
|
||||
return
|
||||
} else if git.IsErrPushRejected(err) {
|
||||
errPushRej := err.(*git.ErrPushRejected)
|
||||
if len(errPushRej.Message) == 0 {
|
||||
ctx.Error(http.StatusConflict, "Merge", "PushRejected without remote error message")
|
||||
return
|
||||
} else {
|
||||
ctx.Error(http.StatusConflict, "Merge", "PushRejected with remote message: "+errPushRej.Message)
|
||||
}
|
||||
ctx.Error(http.StatusConflict, "Merge", "PushRejected with remote message: "+errPushRej.Message)
|
||||
return
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "Merge", err)
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "Merge", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Pull request merged: %d", pr.ID)
|
||||
|
||||
if form.DeleteBranchAfterMerge {
|
||||
// Don't cleanup when there are other PR's that use this branch as head branch.
|
||||
exist, err := models.HasUnmergedPullRequestsByHeadInfo(pr.HeadRepoID, pr.HeadBranch)
|
||||
exist, err := models.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
|
||||
return
|
||||
@@ -887,14 +881,14 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||
if ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.HeadRepoID && ctx.Repo.GitRepo != nil {
|
||||
headRepo = ctx.Repo.GitRepo
|
||||
} else {
|
||||
headRepo, err = git.OpenRepositoryCtx(ctx, pr.HeadRepo.RepoPath())
|
||||
headRepo, err = git.OpenRepository(ctx, pr.HeadRepo.RepoPath())
|
||||
if err != nil {
|
||||
ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err)
|
||||
return
|
||||
}
|
||||
defer headRepo.Close()
|
||||
}
|
||||
if err := repo_service.DeleteBranch(ctx.User, pr.HeadRepo, headRepo, pr.HeadBranch); err != nil {
|
||||
if err := repo_service.DeleteBranch(ctx.Doer, pr.HeadRepo, headRepo, pr.HeadBranch); err != nil {
|
||||
switch {
|
||||
case git.IsErrBranchNotExist(err):
|
||||
ctx.NotFound(err)
|
||||
@@ -907,7 +901,7 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := models.AddDeletePRBranchComment(ctx.User, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil {
|
||||
if err := models.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil {
|
||||
// Do not fail here as branch has already been deleted
|
||||
log.Error("DeleteBranch: %v", err)
|
||||
}
|
||||
@@ -981,7 +975,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
|
||||
headRepo = ctx.Repo.Repository
|
||||
headGitRepo = ctx.Repo.GitRepo
|
||||
} else {
|
||||
headGitRepo, err = git.OpenRepositoryCtx(ctx, repo_model.RepoPath(headUser.Name, headRepo.Name))
|
||||
headGitRepo, err = git.OpenRepository(ctx, repo_model.RepoPath(headUser.Name, headRepo.Name))
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
|
||||
return nil, nil, nil, nil, "", ""
|
||||
@@ -989,7 +983,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
|
||||
}
|
||||
|
||||
// user should have permission to read baseRepo's codes and pulls, NOT headRepo's
|
||||
permBase, err := models.GetUserRepoPermission(baseRepo, ctx.User)
|
||||
permBase, err := models.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
|
||||
if err != nil {
|
||||
headGitRepo.Close()
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
|
||||
@@ -998,7 +992,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
|
||||
if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) {
|
||||
if log.IsTrace() {
|
||||
log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v",
|
||||
ctx.User,
|
||||
ctx.Doer,
|
||||
baseRepo,
|
||||
permBase)
|
||||
}
|
||||
@@ -1008,7 +1002,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
|
||||
}
|
||||
|
||||
// user should have permission to read headrepo's codes
|
||||
permHead, err := models.GetUserRepoPermission(headRepo, ctx.User)
|
||||
permHead, err := models.GetUserRepoPermission(ctx, headRepo, ctx.Doer)
|
||||
if err != nil {
|
||||
headGitRepo.Close()
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
|
||||
@@ -1017,7 +1011,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
|
||||
if !permHead.CanRead(unit.TypeCode) {
|
||||
if log.IsTrace() {
|
||||
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
|
||||
ctx.User,
|
||||
ctx.Doer,
|
||||
headRepo,
|
||||
permHead)
|
||||
}
|
||||
@@ -1109,18 +1103,18 @@ func UpdatePullRequest(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = pr.LoadBaseRepo(); err != nil {
|
||||
if err = pr.LoadBaseRepoCtx(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
|
||||
return
|
||||
}
|
||||
if err = pr.LoadHeadRepo(); err != nil {
|
||||
if err = pr.LoadHeadRepoCtx(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
|
||||
return
|
||||
}
|
||||
|
||||
rebase := ctx.FormString("style") == "rebase"
|
||||
|
||||
allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(pr, ctx.User)
|
||||
allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, pr, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsUserAllowedToMerge", err)
|
||||
return
|
||||
@@ -1134,7 +1128,7 @@ func UpdatePullRequest(ctx *context.APIContext) {
|
||||
// default merge commit message
|
||||
message := fmt.Sprintf("Merge branch '%s' into %s", pr.BaseBranch, pr.HeadBranch)
|
||||
|
||||
if err = pull_service.Update(ctx, pr, ctx.User, message, rebase); err != nil {
|
||||
if err = pull_service.Update(ctx, pr, ctx.Doer, message, rebase); err != nil {
|
||||
if models.IsErrMergeConflicts(err) {
|
||||
ctx.Error(http.StatusConflict, "Update", "merge failed because of conflict")
|
||||
return
|
||||
@@ -1149,6 +1143,78 @@ func UpdatePullRequest(ctx *context.APIContext) {
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// MergePullRequest cancel an auto merge scheduled for a given PullRequest by index
|
||||
func CancelScheduledAutoMerge(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/merge repository repoCancelScheduledAutoMerge
|
||||
// ---
|
||||
// summary: Cancel the scheduled auto merge for the given pull request
|
||||
// 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: index
|
||||
// in: path
|
||||
// description: index of the pull request to merge
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
pullIndex := ctx.ParamsInt64(":index")
|
||||
pull, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, pullIndex)
|
||||
if err != nil {
|
||||
if models.IsErrPullRequestNotExist(err) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
exist, autoMerge, err := pull_model.GetScheduledMergeByPullID(ctx, pull.ID)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Doer.ID != autoMerge.DoerID {
|
||||
allowed, err := models.IsUserRepoAdminCtx(ctx, ctx.Repo.Repository, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
if !allowed {
|
||||
ctx.Error(http.StatusForbidden, "No permission to cancel", "user has no permission to cancel the scheduled auto merge")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := automerge.RemoveScheduledAutoMerge(ctx, ctx.Doer, pull); err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
} else {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// GetPullRequestCommits gets all commits associated with a given PR
|
||||
func GetPullRequestCommits(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/commits repository repoGetPullRequestCommits
|
||||
@@ -1197,7 +1263,7 @@ func GetPullRequestCommits(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := pr.LoadBaseRepo(); err != nil {
|
||||
if err := pr.LoadBaseRepoCtx(ctx); err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
@@ -74,7 +75,7 @@ func ListPullReviews(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = pr.Issue.LoadRepo(); err != nil {
|
||||
if err = pr.Issue.LoadRepo(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadRepo", err)
|
||||
return
|
||||
}
|
||||
@@ -97,7 +98,7 @@ func ListPullReviews(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
apiReviews, err := convert.ToPullReviewList(ctx, allReviews, ctx.User)
|
||||
apiReviews, err := convert.ToPullReviewList(ctx, allReviews, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "convertToPullReviewList", err)
|
||||
return
|
||||
@@ -148,7 +149,7 @@ func GetPullReview(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
apiReview, err := convert.ToPullReview(ctx, review, ctx.User)
|
||||
apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "convertToPullReview", err)
|
||||
return
|
||||
@@ -198,7 +199,7 @@ func GetPullReviewComments(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
apiComments, err := convert.ToPullReviewCommentList(ctx, review, ctx.User)
|
||||
apiComments, err := convert.ToPullReviewCommentList(ctx, review, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "convertToPullReviewCommentList", err)
|
||||
return
|
||||
@@ -250,11 +251,11 @@ func DeletePullReview(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.User == nil {
|
||||
if ctx.Doer == nil {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
if !ctx.User.IsAdmin && ctx.User.ID != review.ReviewerID {
|
||||
if !ctx.Doer.IsAdmin && ctx.Doer.ID != review.ReviewerID {
|
||||
ctx.Error(http.StatusForbidden, "only admin and user itself can delete a review", nil)
|
||||
return
|
||||
}
|
||||
@@ -321,7 +322,7 @@ func CreatePullReview(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := pr.Issue.LoadRepo(); err != nil {
|
||||
if err := pr.Issue.LoadRepo(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "pr.Issue.LoadRepo", err)
|
||||
return
|
||||
}
|
||||
@@ -353,7 +354,7 @@ func CreatePullReview(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if _, err := pull_service.CreateCodeComment(ctx,
|
||||
ctx.User,
|
||||
ctx.Doer,
|
||||
ctx.Repo.GitRepo,
|
||||
pr.Issue,
|
||||
line,
|
||||
@@ -369,14 +370,14 @@ func CreatePullReview(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
// create review and associate all pending review comments
|
||||
review, _, err := pull_service.SubmitReview(ctx, ctx.User, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil)
|
||||
review, _, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
|
||||
return
|
||||
}
|
||||
|
||||
// convert response
|
||||
apiReview, err := convert.ToPullReview(ctx, review, ctx.User)
|
||||
apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "convertToPullReview", err)
|
||||
return
|
||||
@@ -457,14 +458,14 @@ func SubmitPullReview(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
// create review and associate all pending review comments
|
||||
review, _, err = pull_service.SubmitReview(ctx, ctx.User, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil)
|
||||
review, _, err = pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
|
||||
return
|
||||
}
|
||||
|
||||
// convert response
|
||||
apiReview, err := convert.ToPullReview(ctx, review, ctx.User)
|
||||
apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "convertToPullReview", err)
|
||||
return
|
||||
@@ -486,7 +487,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even
|
||||
switch event {
|
||||
case api.ReviewStateApproved:
|
||||
// can not approve your own PR
|
||||
if pr.Issue.IsPoster(ctx.User.ID) {
|
||||
if pr.Issue.IsPoster(ctx.Doer.ID) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("approve your own pull is not allowed"))
|
||||
return -1, true
|
||||
}
|
||||
@@ -495,7 +496,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even
|
||||
|
||||
case api.ReviewStateRequestChanges:
|
||||
// can not reject your own PR
|
||||
if pr.Issue.IsPoster(ctx.User.ID) {
|
||||
if pr.Issue.IsPoster(ctx.Doer.ID) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("reject your own pull is not allowed"))
|
||||
return -1, true
|
||||
}
|
||||
@@ -551,7 +552,7 @@ func prepareSingleReview(ctx *context.APIContext) (*models.Review, *models.PullR
|
||||
}
|
||||
|
||||
// make sure that the user has access to this review if it is pending
|
||||
if review.Type == models.ReviewTypePending && review.ReviewerID != ctx.User.ID && !ctx.User.IsAdmin {
|
||||
if review.Type == models.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
|
||||
ctx.NotFound("GetReviewByID")
|
||||
return nil, nil, true
|
||||
}
|
||||
@@ -656,14 +657,14 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
|
||||
return
|
||||
}
|
||||
|
||||
if err := pr.Issue.LoadRepo(); err != nil {
|
||||
if err := pr.Issue.LoadRepo(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "pr.Issue.LoadRepo", err)
|
||||
return
|
||||
}
|
||||
|
||||
reviewers := make([]*user_model.User, 0, len(opts.Reviewers))
|
||||
|
||||
permDoer, err := models.GetUserRepoPermission(pr.Issue.Repo, ctx.User)
|
||||
permDoer, err := models.GetUserRepoPermission(ctx, pr.Issue.Repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
|
||||
return
|
||||
@@ -686,7 +687,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
|
||||
return
|
||||
}
|
||||
|
||||
err = issue_service.IsValidReviewRequest(reviewer, ctx.User, isAdd, pr.Issue, &permDoer)
|
||||
err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, isAdd, pr.Issue, &permDoer)
|
||||
if err != nil {
|
||||
if models.IsErrNotValidReviewRequest(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err)
|
||||
@@ -705,7 +706,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
|
||||
}
|
||||
|
||||
for _, reviewer := range reviewers {
|
||||
comment, err := issue_service.ReviewRequest(pr.Issue, ctx.User, reviewer, isAdd)
|
||||
comment, err := issue_service.ReviewRequest(pr.Issue, ctx.Doer, reviewer, isAdd)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ReviewRequest", err)
|
||||
return
|
||||
@@ -722,12 +723,12 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
|
||||
|
||||
if ctx.Repo.Repository.Owner.IsOrganization() && len(opts.TeamReviewers) > 0 {
|
||||
|
||||
teamReviewers := make([]*models.Team, 0, len(opts.TeamReviewers))
|
||||
teamReviewers := make([]*organization.Team, 0, len(opts.TeamReviewers))
|
||||
for _, t := range opts.TeamReviewers {
|
||||
var teamReviewer *models.Team
|
||||
teamReviewer, err = models.GetTeam(ctx.Repo.Owner.ID, t)
|
||||
var teamReviewer *organization.Team
|
||||
teamReviewer, err = organization.GetTeam(ctx.Repo.Owner.ID, t)
|
||||
if err != nil {
|
||||
if models.IsErrTeamNotExist(err) {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.NotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t))
|
||||
return
|
||||
}
|
||||
@@ -735,7 +736,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
|
||||
return
|
||||
}
|
||||
|
||||
err = issue_service.IsValidTeamReviewRequest(teamReviewer, ctx.User, isAdd, pr.Issue)
|
||||
err = issue_service.IsValidTeamReviewRequest(ctx, teamReviewer, ctx.Doer, isAdd, pr.Issue)
|
||||
if err != nil {
|
||||
if models.IsErrNotValidReviewRequest(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err)
|
||||
@@ -749,7 +750,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
|
||||
}
|
||||
|
||||
for _, teamReviewer := range teamReviewers {
|
||||
comment, err := issue_service.TeamReviewRequest(pr.Issue, ctx.User, teamReviewer, isAdd)
|
||||
comment, err := issue_service.TeamReviewRequest(pr.Issue, ctx.Doer, teamReviewer, isAdd)
|
||||
if err != nil {
|
||||
ctx.ServerError("TeamReviewRequest", err)
|
||||
return
|
||||
@@ -766,7 +767,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
|
||||
}
|
||||
|
||||
if isAdd {
|
||||
apiReviews, err := convert.ToPullReviewList(ctx, reviews, ctx.User)
|
||||
apiReviews, err := convert.ToPullReviewList(ctx, reviews, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "convertToPullReviewList", err)
|
||||
return
|
||||
@@ -884,7 +885,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss bool) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := pull_service.DismissReview(ctx, review.ID, msg, ctx.User, isDismiss)
|
||||
_, err := pull_service.DismissReview(ctx, review.ID, msg, ctx.Doer, isDismiss)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "pull_service.DismissReview", err)
|
||||
return
|
||||
@@ -896,7 +897,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss bool) {
|
||||
}
|
||||
|
||||
// convert response
|
||||
apiReview, err := convert.ToPullReview(ctx, review, ctx.User)
|
||||
apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "convertToPullReview", err)
|
||||
return
|
||||
|
||||
@@ -191,8 +191,8 @@ func CreateRelease(ctx *context.APIContext) {
|
||||
}
|
||||
rel = &models.Release{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
PublisherID: ctx.User.ID,
|
||||
Publisher: ctx.User,
|
||||
PublisherID: ctx.Doer.ID,
|
||||
Publisher: ctx.Doer,
|
||||
TagName: form.TagName,
|
||||
Target: form.Target,
|
||||
Title: form.Title,
|
||||
@@ -220,12 +220,12 @@ func CreateRelease(ctx *context.APIContext) {
|
||||
rel.Note = form.Note
|
||||
rel.IsDraft = form.IsDraft
|
||||
rel.IsPrerelease = form.IsPrerelease
|
||||
rel.PublisherID = ctx.User.ID
|
||||
rel.PublisherID = ctx.Doer.ID
|
||||
rel.IsTag = false
|
||||
rel.Repo = ctx.Repo.Repository
|
||||
rel.Publisher = ctx.User
|
||||
rel.Publisher = ctx.Doer
|
||||
|
||||
if err = releaseservice.UpdateRelease(ctx.User, ctx.Repo.GitRepo, rel, nil, nil, nil); err != nil {
|
||||
if err = releaseservice.UpdateRelease(ctx.Doer, ctx.Repo.GitRepo, rel, nil, nil, nil); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateRelease", err)
|
||||
return
|
||||
}
|
||||
@@ -300,7 +300,7 @@ func EditRelease(ctx *context.APIContext) {
|
||||
if form.IsPrerelease != nil {
|
||||
rel.IsPrerelease = *form.IsPrerelease
|
||||
}
|
||||
if err := releaseservice.UpdateRelease(ctx.User, ctx.Repo.GitRepo, rel, nil, nil, nil); err != nil {
|
||||
if err := releaseservice.UpdateRelease(ctx.Doer, ctx.Repo.GitRepo, rel, nil, nil, nil); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateRelease", err)
|
||||
return
|
||||
}
|
||||
@@ -356,7 +356,7 @@ func DeleteRelease(ctx *context.APIContext) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
if err := releaseservice.DeleteReleaseByID(ctx, id, ctx.User, false); err != nil {
|
||||
if err := releaseservice.DeleteReleaseByID(ctx, id, ctx.Doer, false); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteReleaseByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
// Create a new attachment and save the file
|
||||
attach, err := attachment.UploadAttachment(file, ctx.User.ID, release.RepoID, releaseID, filename, setting.Repository.Release.AllowedTypes)
|
||||
attach, err := attachment.UploadAttachment(file, ctx.Doer.ID, release.RepoID, releaseID, filename, setting.Repository.Release.AllowedTypes)
|
||||
if err != nil {
|
||||
if upload.IsErrFileTypeForbidden(err) {
|
||||
ctx.Error(http.StatusBadRequest, "DetectContentType", err)
|
||||
|
||||
@@ -110,7 +110,7 @@ func DeleteReleaseByTag(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = releaseservice.DeleteReleaseByID(ctx, release.ID, ctx.User, false); err != nil {
|
||||
if err = releaseservice.DeleteReleaseByID(ctx, release.ID, ctx.Doer, false); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteReleaseByID", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@@ -30,23 +32,6 @@ import (
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
var searchOrderByMap = map[string]map[string]db.SearchOrderBy{
|
||||
"asc": {
|
||||
"alpha": db.SearchOrderByAlphabetically,
|
||||
"created": db.SearchOrderByOldest,
|
||||
"updated": db.SearchOrderByLeastUpdated,
|
||||
"size": db.SearchOrderBySize,
|
||||
"id": db.SearchOrderByID,
|
||||
},
|
||||
"desc": {
|
||||
"alpha": db.SearchOrderByAlphabeticallyReverse,
|
||||
"created": db.SearchOrderByNewest,
|
||||
"updated": db.SearchOrderByRecentUpdated,
|
||||
"size": db.SearchOrderBySizeReverse,
|
||||
"id": db.SearchOrderByIDReverse,
|
||||
},
|
||||
}
|
||||
|
||||
// Search repositories via options
|
||||
func Search(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/search repository repoSearch
|
||||
@@ -139,7 +124,7 @@ func Search(ctx *context.APIContext) {
|
||||
|
||||
opts := &models.SearchRepoOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
Actor: ctx.User,
|
||||
Actor: ctx.Doer,
|
||||
Keyword: ctx.FormTrim("q"),
|
||||
OwnerID: ctx.FormInt64("uid"),
|
||||
PriorityOwnerID: ctx.FormInt64("priority_owner_id"),
|
||||
@@ -192,7 +177,7 @@ func Search(ctx *context.APIContext) {
|
||||
if len(sortOrder) == 0 {
|
||||
sortOrder = "asc"
|
||||
}
|
||||
if searchModeMap, ok := searchOrderByMap[sortOrder]; ok {
|
||||
if searchModeMap, ok := context.SearchOrderByMap[sortOrder]; ok {
|
||||
if orderBy, ok := searchModeMap[sortMode]; ok {
|
||||
opts.OrderBy = orderBy
|
||||
} else {
|
||||
@@ -217,14 +202,14 @@ func Search(ctx *context.APIContext) {
|
||||
|
||||
results := make([]*api.Repository, len(repos))
|
||||
for i, repo := range repos {
|
||||
if err = repo.GetOwner(db.DefaultContext); err != nil {
|
||||
if err = repo.GetOwner(ctx); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, api.SearchError{
|
||||
OK: false,
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
accessMode, err := models.AccessLevel(ctx.User, repo)
|
||||
accessMode, err := models.AccessLevel(ctx.Doer, repo)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, api.SearchError{
|
||||
OK: false,
|
||||
@@ -233,7 +218,6 @@ func Search(ctx *context.APIContext) {
|
||||
}
|
||||
results[i] = convert.ToRepo(repo, accessMode)
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(int(count), opts.PageSize)
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, api.SearchResults{
|
||||
@@ -247,7 +231,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
|
||||
if opt.AutoInit && opt.Readme == "" {
|
||||
opt.Readme = "Default"
|
||||
}
|
||||
repo, err := repo_service.CreateRepository(ctx.User, owner, models.CreateRepoOptions{
|
||||
repo, err := repo_service.CreateRepository(ctx.Doer, owner, models.CreateRepoOptions{
|
||||
Name: opt.Name,
|
||||
Description: opt.Description,
|
||||
IssueLabels: opt.IssueLabels,
|
||||
@@ -264,7 +248,8 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
|
||||
if repo_model.IsErrRepoAlreadyExist(err) {
|
||||
ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.")
|
||||
} else if db.IsErrNameReserved(err) ||
|
||||
db.IsErrNamePatternNotAllowed(err) {
|
||||
db.IsErrNamePatternNotAllowed(err) ||
|
||||
repo_module.IsErrIssueLabelTemplateLoad(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateRepository", err)
|
||||
@@ -303,12 +288,12 @@ func Create(ctx *context.APIContext) {
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
opt := web.GetForm(ctx).(*api.CreateRepoOption)
|
||||
if ctx.User.IsOrganization() {
|
||||
if ctx.Doer.IsOrganization() {
|
||||
// Shouldn't reach this condition, but just in case.
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", "not allowed creating repository for organization")
|
||||
return
|
||||
}
|
||||
CreateUserRepo(ctx, ctx.User, *opt)
|
||||
CreateUserRepo(ctx, ctx.Doer, *opt)
|
||||
}
|
||||
|
||||
// Generate Create a repository using a template
|
||||
@@ -353,21 +338,22 @@ func Generate(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.User.IsOrganization() {
|
||||
if ctx.Doer.IsOrganization() {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", "not allowed creating repository for organization")
|
||||
return
|
||||
}
|
||||
|
||||
opts := models.GenerateRepoOptions{
|
||||
Name: form.Name,
|
||||
Description: form.Description,
|
||||
Private: form.Private,
|
||||
GitContent: form.GitContent,
|
||||
Topics: form.Topics,
|
||||
GitHooks: form.GitHooks,
|
||||
Webhooks: form.Webhooks,
|
||||
Avatar: form.Avatar,
|
||||
IssueLabels: form.Labels,
|
||||
Name: form.Name,
|
||||
DefaultBranch: form.DefaultBranch,
|
||||
Description: form.Description,
|
||||
Private: form.Private,
|
||||
GitContent: form.GitContent,
|
||||
Topics: form.Topics,
|
||||
GitHooks: form.GitHooks,
|
||||
Webhooks: form.Webhooks,
|
||||
Avatar: form.Avatar,
|
||||
IssueLabels: form.Labels,
|
||||
}
|
||||
|
||||
if !opts.IsValid() {
|
||||
@@ -375,7 +361,7 @@ func Generate(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
ctxUser := ctx.User
|
||||
ctxUser := ctx.Doer
|
||||
var err error
|
||||
if form.Owner != ctxUser.Name {
|
||||
ctxUser, err = user_model.GetUserByName(form.Owner)
|
||||
@@ -391,13 +377,13 @@ func Generate(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.User.IsAdmin && !ctxUser.IsOrganization() {
|
||||
if !ctx.Doer.IsAdmin && !ctxUser.IsOrganization() {
|
||||
ctx.Error(http.StatusForbidden, "", "Only admin can generate repository for other user.")
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.User.IsAdmin {
|
||||
canCreate, err := models.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx.User.ID)
|
||||
if !ctx.Doer.IsAdmin {
|
||||
canCreate, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("CanCreateOrgRepo", err)
|
||||
return
|
||||
@@ -408,7 +394,7 @@ func Generate(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
repo, err := repo_service.GenerateRepository(ctx.User, ctxUser, ctx.Repo.Repository, opts)
|
||||
repo, err := repo_service.GenerateRepository(ctx.Doer, ctxUser, ctx.Repo.Repository, opts)
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoAlreadyExist(err) {
|
||||
ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.")
|
||||
@@ -483,9 +469,9 @@ func CreateOrgRepo(ctx *context.APIContext) {
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
opt := web.GetForm(ctx).(*api.CreateRepoOption)
|
||||
org, err := models.GetOrgByName(ctx.Params(":org"))
|
||||
org, err := organization.GetOrgByName(ctx.Params(":org"))
|
||||
if err != nil {
|
||||
if models.IsErrOrgNotExist(err) {
|
||||
if organization.IsErrOrgNotExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetOrgByName", err)
|
||||
@@ -493,13 +479,13 @@ func CreateOrgRepo(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !models.HasOrgOrUserVisible(org.AsUser(), ctx.User) {
|
||||
if !organization.HasOrgOrUserVisible(ctx, org.AsUser(), ctx.Doer) {
|
||||
ctx.NotFound("HasOrgOrUserVisible", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.User.IsAdmin {
|
||||
canCreate, err := org.CanCreateOrgRepo(ctx.User.ID)
|
||||
if !ctx.Doer.IsAdmin {
|
||||
canCreate, err := org.CanCreateOrgRepo(ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CanCreateOrgRepo", err)
|
||||
return
|
||||
@@ -569,7 +555,7 @@ func GetByID(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
perm, err := models.GetUserRepoPermission(repo, ctx.User)
|
||||
perm, err := models.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "AccessLevel", err)
|
||||
return
|
||||
@@ -629,7 +615,7 @@ func Edit(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if opts.MirrorInterval != nil {
|
||||
if err := updateMirrorInterval(ctx, opts); err != nil {
|
||||
if err := updateMirror(ctx, opts); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -653,7 +639,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
|
||||
}
|
||||
// Check if repository name has been changed and not just a case change
|
||||
if repo.LowerName != strings.ToLower(newRepoName) {
|
||||
if err := repo_service.ChangeRepositoryName(ctx.User, repo, newRepoName); err != nil {
|
||||
if err := repo_service.ChangeRepositoryName(ctx.Doer, repo, newRepoName); err != nil {
|
||||
switch {
|
||||
case repo_model.IsErrRepoAlreadyExist(err):
|
||||
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is already taken [name: %s]", newRepoName), err)
|
||||
@@ -694,7 +680,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
|
||||
|
||||
visibilityChanged = repo.IsPrivate != *opts.Private
|
||||
// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
|
||||
if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.User.IsAdmin {
|
||||
if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.Doer.IsAdmin {
|
||||
err := fmt.Errorf("cannot change private repository to public")
|
||||
ctx.Error(http.StatusUnprocessableEntity, "Force Private enabled", err)
|
||||
return err
|
||||
@@ -709,7 +695,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
|
||||
|
||||
if ctx.Repo.GitRepo == nil && !repo.IsEmpty {
|
||||
var err error
|
||||
ctx.Repo.GitRepo, err = git.OpenRepositoryCtx(ctx, ctx.Repo.Repository.RepoPath())
|
||||
ctx.Repo.GitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err)
|
||||
return err
|
||||
@@ -958,37 +944,67 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateMirrorInterval updates the repo's mirror Interval
|
||||
func updateMirrorInterval(ctx *context.APIContext, opts api.EditRepoOption) error {
|
||||
// updateMirror updates a repo's mirror Interval and EnablePrune
|
||||
func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error {
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
// only update mirror if interval or enable prune are provided
|
||||
if opts.MirrorInterval == nil && opts.EnablePrune == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// these values only make sense if the repo is a mirror
|
||||
if !repo.IsMirror {
|
||||
err := fmt.Errorf("repo is not a mirror, can not change mirror interval")
|
||||
ctx.Error(http.StatusUnprocessableEntity, err.Error(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
// get the mirror from the repo
|
||||
mirror, err := repo_model.GetMirrorByRepoID(repo.ID)
|
||||
if err != nil {
|
||||
log.Error("Failed to get mirror: %s", err)
|
||||
ctx.Error(http.StatusInternalServerError, "MirrorInterval", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// update MirrorInterval
|
||||
if opts.MirrorInterval != nil {
|
||||
if !repo.IsMirror {
|
||||
err := fmt.Errorf("repo is not a mirror, can not change mirror interval")
|
||||
ctx.Error(http.StatusUnprocessableEntity, err.Error(), err)
|
||||
return err
|
||||
}
|
||||
mirror, err := repo_model.GetMirrorByRepoID(repo.ID)
|
||||
|
||||
// MirrorInterval should be a duration
|
||||
interval, err := time.ParseDuration(*opts.MirrorInterval)
|
||||
if err != nil {
|
||||
log.Error("Failed to get mirror: %s", err)
|
||||
ctx.Error(http.StatusInternalServerError, "MirrorInterval", err)
|
||||
return err
|
||||
}
|
||||
if interval, err := time.ParseDuration(*opts.MirrorInterval); err == nil {
|
||||
mirror.Interval = interval
|
||||
mirror.Repo = repo
|
||||
if err := repo_model.UpdateMirror(mirror); err != nil {
|
||||
log.Error("Failed to Set Mirror Interval: %s", err)
|
||||
ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err)
|
||||
return err
|
||||
}
|
||||
log.Trace("Repository %s/%s Mirror Interval was Updated to %s", ctx.Repo.Owner.Name, repo.Name, interval)
|
||||
} else {
|
||||
log.Error("Wrong format for MirrorInternal Sent: %s", err)
|
||||
ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure the provided duration is not too short
|
||||
if interval != 0 && interval < setting.Mirror.MinInterval {
|
||||
err := fmt.Errorf("invalid mirror interval: %s is below minimum interval: %s", interval, setting.Mirror.MinInterval)
|
||||
ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err)
|
||||
return err
|
||||
}
|
||||
|
||||
mirror.Interval = interval
|
||||
mirror.Repo = repo
|
||||
mirror.ScheduleNextUpdate()
|
||||
log.Trace("Repository %s Mirror[%d] Set Interval: %s NextUpdateUnix: %s", repo.FullName(), mirror.ID, interval, mirror.NextUpdateUnix)
|
||||
}
|
||||
|
||||
// update EnablePrune
|
||||
if opts.EnablePrune != nil {
|
||||
mirror.EnablePrune = *opts.EnablePrune
|
||||
log.Trace("Repository %s Mirror[%d] Set EnablePrune: %t", repo.FullName(), mirror.ID, mirror.EnablePrune)
|
||||
}
|
||||
|
||||
// finally update the mirror in the DB
|
||||
if err := repo_model.UpdateMirror(mirror); err != nil {
|
||||
log.Error("Failed to Set Mirror Interval: %s", err)
|
||||
ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1019,7 +1035,7 @@ func Delete(ctx *context.APIContext) {
|
||||
owner := ctx.Repo.Owner
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
canDelete, err := models.CanUserDelete(repo, ctx.User)
|
||||
canDelete, err := models.CanUserDelete(repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CanUserDelete", err)
|
||||
return
|
||||
@@ -1032,7 +1048,7 @@ func Delete(ctx *context.APIContext) {
|
||||
ctx.Repo.GitRepo.Close()
|
||||
}
|
||||
|
||||
if err := repo_service.DeleteRepository(ctx, ctx.User, repo, true); err != nil {
|
||||
if err := repo_service.DeleteRepository(ctx, ctx.Doer, repo, true); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteRepository", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestRepoEdit(t *testing.T) {
|
||||
ctx := test.MockContext(t, "user2/repo1")
|
||||
test.LoadRepo(t, ctx, 1)
|
||||
test.LoadUser(t, ctx, 2)
|
||||
ctx.Repo.Owner = ctx.User
|
||||
ctx.Repo.Owner = ctx.Doer
|
||||
description := "new description"
|
||||
website := "http://wwww.newwebsite.com"
|
||||
private := true
|
||||
@@ -71,7 +71,7 @@ func TestRepoEditNameChange(t *testing.T) {
|
||||
ctx := test.MockContext(t, "user2/repo1")
|
||||
test.LoadRepo(t, ctx, 1)
|
||||
test.LoadUser(t, ctx, 2)
|
||||
ctx.Repo.Owner = ctx.User
|
||||
ctx.Repo.Owner = ctx.Doer
|
||||
name := "newname"
|
||||
opts := api.EditRepoOption{
|
||||
Name: &name,
|
||||
|
||||
@@ -51,7 +51,7 @@ func ListStargazers(ctx *context.APIContext) {
|
||||
}
|
||||
users := make([]*api.User, len(stargazers))
|
||||
for i, stargazer := range stargazers {
|
||||
users[i] = convert.ToUser(stargazer, ctx.User)
|
||||
users[i] = convert.ToUser(stargazer, ctx.Doer)
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumStars))
|
||||
|
||||
@@ -62,7 +62,7 @@ func NewCommitStatus(ctx *context.APIContext) {
|
||||
Description: form.Description,
|
||||
Context: form.Context,
|
||||
}
|
||||
if err := files_service.CreateCommitStatus(ctx, ctx.Repo.Repository, ctx.User, sha, status); err != nil {
|
||||
if err := files_service.CreateCommitStatus(ctx, ctx.Repo.Repository, ctx.Doer, sha, status); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateCommitStatus", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func ListSubscribers(ctx *context.APIContext) {
|
||||
}
|
||||
users := make([]*api.User, len(subscribers))
|
||||
for i, subscriber := range subscribers {
|
||||
users[i] = convert.ToUser(subscriber, ctx.User)
|
||||
users[i] = convert.ToUser(subscriber, ctx.Doer)
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumWatches))
|
||||
|
||||
@@ -191,7 +191,7 @@ func CreateTag(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := releaseservice.CreateNewTag(ctx, ctx.User, ctx.Repo.Repository, commit.ID.String(), form.TagName, form.Message); err != nil {
|
||||
if err := releaseservice.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, commit.ID.String(), form.TagName, form.Message); err != nil {
|
||||
if models.IsErrTagAlreadyExists(err) {
|
||||
ctx.Error(http.StatusConflict, "tag exist", err)
|
||||
return
|
||||
@@ -255,7 +255,7 @@ func DeleteTag(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = releaseservice.DeleteReleaseByID(ctx, tag.ID, ctx.User, true); err != nil {
|
||||
if err = releaseservice.DeleteReleaseByID(ctx, tag.ID, ctx.Doer, true); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteReleaseByID", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
@@ -41,7 +42,7 @@ func ListTeams(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
teams, err := models.GetRepoTeams(ctx.Repo.Repository)
|
||||
teams, err := organization.GetRepoTeams(ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
@@ -101,7 +102,7 @@ func IsTeam(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if team.HasRepository(ctx.Repo.Repository.ID) {
|
||||
if models.HasRepository(team, ctx.Repo.Repository.ID) {
|
||||
if err := team.GetUnits(); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUnits", err)
|
||||
return
|
||||
@@ -196,20 +197,20 @@ func changeRepoTeam(ctx *context.APIContext, add bool) {
|
||||
return
|
||||
}
|
||||
|
||||
repoHasTeam := team.HasRepository(ctx.Repo.Repository.ID)
|
||||
repoHasTeam := models.HasRepository(team, ctx.Repo.Repository.ID)
|
||||
var err error
|
||||
if add {
|
||||
if repoHasTeam {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "alreadyAdded", fmt.Errorf("team '%s' is already added to repo", team.Name))
|
||||
return
|
||||
}
|
||||
err = team.AddRepository(ctx.Repo.Repository)
|
||||
err = models.AddRepository(team, ctx.Repo.Repository)
|
||||
} else {
|
||||
if !repoHasTeam {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "notAdded", fmt.Errorf("team '%s' was not added to repo", team.Name))
|
||||
return
|
||||
}
|
||||
err = team.RemoveRepository(ctx.Repo.Repository.ID)
|
||||
err = models.RemoveRepository(team, ctx.Repo.Repository.ID)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
@@ -219,10 +220,10 @@ func changeRepoTeam(ctx *context.APIContext, add bool) {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func getTeamByParam(ctx *context.APIContext) *models.Team {
|
||||
team, err := models.GetTeam(ctx.Repo.Owner.ID, ctx.Params(":team"))
|
||||
func getTeamByParam(ctx *context.APIContext) *organization.Team {
|
||||
team, err := organization.GetTeam(ctx.Repo.Owner.ID, ctx.Params(":team"))
|
||||
if err != nil {
|
||||
if models.IsErrTeamNotExist(err) {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.Error(http.StatusNotFound, "TeamNotExit", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@@ -67,23 +68,23 @@ func Transfer(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if newOwner.Type == user_model.UserTypeOrganization {
|
||||
if !ctx.User.IsAdmin && newOwner.Visibility == api.VisibleTypePrivate && !models.OrgFromUser(newOwner).HasMemberWithUserID(ctx.User.ID) {
|
||||
if !ctx.Doer.IsAdmin && newOwner.Visibility == api.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx.Doer.ID) {
|
||||
// The user shouldn't know about this organization
|
||||
ctx.Error(http.StatusNotFound, "", "The new owner does not exist or cannot be found")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var teams []*models.Team
|
||||
var teams []*organization.Team
|
||||
if opts.TeamIDs != nil {
|
||||
if !newOwner.IsOrganization() {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "repoTransfer", "Teams can only be added to organization-owned repositories")
|
||||
return
|
||||
}
|
||||
|
||||
org := convert.ToOrganization(models.OrgFromUser(newOwner))
|
||||
org := convert.ToOrganization(organization.OrgFromUser(newOwner))
|
||||
for _, tID := range *opts.TeamIDs {
|
||||
team, err := models.GetTeamByID(tID)
|
||||
team, err := organization.GetTeamByID(tID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "team", fmt.Errorf("team %d not found", tID))
|
||||
return
|
||||
@@ -103,14 +104,16 @@ func Transfer(ctx *context.APIContext) {
|
||||
ctx.Repo.GitRepo = nil
|
||||
}
|
||||
|
||||
if err := repo_service.StartRepositoryTransfer(ctx.User, newOwner, ctx.Repo.Repository, teams); err != nil {
|
||||
oldFullname := ctx.Repo.Repository.FullName()
|
||||
|
||||
if err := repo_service.StartRepositoryTransfer(ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil {
|
||||
if models.IsErrRepoTransferInProgress(err) {
|
||||
ctx.Error(http.StatusConflict, "CreatePendingRepositoryTransfer", err)
|
||||
ctx.Error(http.StatusConflict, "StartRepositoryTransfer", err)
|
||||
return
|
||||
}
|
||||
|
||||
if repo_model.IsErrRepoAlreadyExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "CreatePendingRepositoryTransfer", err)
|
||||
ctx.Error(http.StatusUnprocessableEntity, "StartRepositoryTransfer", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -119,12 +122,12 @@ func Transfer(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
|
||||
log.Trace("Repository transfer initiated: %s -> %s", ctx.Repo.Repository.FullName(), newOwner.Name)
|
||||
log.Trace("Repository transfer initiated: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
|
||||
ctx.JSON(http.StatusCreated, convert.ToRepo(ctx.Repo.Repository, perm.AccessModeAdmin))
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Repository transferred: %s -> %s", ctx.Repo.Repository.FullName(), newOwner.Name)
|
||||
log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
|
||||
ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx.Repo.Repository, perm.AccessModeAdmin))
|
||||
}
|
||||
|
||||
@@ -218,7 +221,7 @@ func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if !repoTransfer.CanUserAcceptTransfer(ctx.User) {
|
||||
if !repoTransfer.CanUserAcceptTransfer(ctx.Doer) {
|
||||
ctx.Error(http.StatusForbidden, "CanUserAcceptTransfer", nil)
|
||||
return fmt.Errorf("user does not have permissions to do this")
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func NewWikiPage(ctx *context.APIContext) {
|
||||
}
|
||||
form.ContentBase64 = string(content)
|
||||
|
||||
if err := wiki_service.AddWikiPage(ctx, ctx.User, ctx.Repo.Repository, wikiName, form.ContentBase64, form.Message); err != nil {
|
||||
if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.ContentBase64, form.Message); err != nil {
|
||||
if models.IsErrWikiReservedName(err) {
|
||||
ctx.Error(http.StatusBadRequest, "IsErrWikiReservedName", err)
|
||||
} else if models.IsErrWikiAlreadyExist(err) {
|
||||
@@ -144,7 +144,7 @@ func EditWikiPage(ctx *context.APIContext) {
|
||||
}
|
||||
form.ContentBase64 = string(content)
|
||||
|
||||
if err := wiki_service.EditWikiPage(ctx, ctx.User, ctx.Repo.Repository, oldWikiName, newWikiName, form.ContentBase64, form.Message); err != nil {
|
||||
if err := wiki_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, oldWikiName, newWikiName, form.ContentBase64, form.Message); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "EditWikiPage", err)
|
||||
return
|
||||
}
|
||||
@@ -233,7 +233,7 @@ func DeleteWikiPage(ctx *context.APIContext) {
|
||||
|
||||
wikiName := wiki_service.NormalizeWikiName(ctx.Params(":pageName"))
|
||||
|
||||
if err := wiki_service.DeleteWikiPage(ctx, ctx.User, ctx.Repo.Repository, wikiName); err != nil {
|
||||
if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil {
|
||||
if err.Error() == "file does not exist" {
|
||||
ctx.NotFound(err)
|
||||
return
|
||||
@@ -458,7 +458,7 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error)
|
||||
// findWikiRepoCommit opens the wiki repo and returns the latest commit, writing to context on error.
|
||||
// The caller is responsible for closing the returned repo again
|
||||
func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) {
|
||||
wikiRepo, err := git.OpenRepositoryCtx(ctx, ctx.Repo.Repository.WikiPath())
|
||||
wikiRepo, err := git.OpenRepository(ctx, ctx.Repo.Repository.WikiPath())
|
||||
if err != nil {
|
||||
|
||||
if git.IsErrNotExist(err) || err.Error() == "no such file or directory" {
|
||||
|
||||
30
routers/api/v1/swagger/package.go
Normal file
30
routers/api/v1/swagger/package.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package swagger
|
||||
|
||||
import (
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
@@ -344,3 +344,10 @@ type swaggerWikiCommitList struct {
|
||||
// in:body
|
||||
Body api.WikiCommitList `json:"body"`
|
||||
}
|
||||
|
||||
// RepoCollaboratorPermission
|
||||
// swagger:response RepoCollaboratorPermission
|
||||
type swaggerRepoCollaboratorPermission struct {
|
||||
// in:body
|
||||
Body api.RepoCollaboratorPermission `json:"body"`
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func ListAccessTokens(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/AccessTokenList"
|
||||
|
||||
opts := models.ListAccessTokensOptions{UserID: ctx.User.ID, ListOptions: utils.GetListOptions(ctx)}
|
||||
opts := models.ListAccessTokensOptions{UserID: ctx.Doer.ID, ListOptions: utils.GetListOptions(ctx)}
|
||||
|
||||
count, err := models.CountAccessTokens(opts)
|
||||
if err != nil {
|
||||
@@ -99,7 +99,7 @@ func CreateAccessToken(ctx *context.APIContext) {
|
||||
form := web.GetForm(ctx).(*api.CreateAccessTokenOption)
|
||||
|
||||
t := &models.AccessToken{
|
||||
UID: ctx.User.ID,
|
||||
UID: ctx.Doer.ID,
|
||||
Name: form.Name,
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ func DeleteAccessToken(ctx *context.APIContext) {
|
||||
if tokenID == 0 {
|
||||
tokens, err := models.ListAccessTokens(models.ListAccessTokensOptions{
|
||||
Name: token,
|
||||
UserID: ctx.User.ID,
|
||||
UserID: ctx.Doer.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ListAccessTokens", err)
|
||||
@@ -180,7 +180,7 @@ func DeleteAccessToken(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.DeleteAccessTokenByID(tokenID, ctx.User.ID); err != nil {
|
||||
if err := models.DeleteAccessTokenByID(tokenID, ctx.Doer.ID); err != nil {
|
||||
if models.IsErrAccessTokenNotExist(err) {
|
||||
ctx.NotFound()
|
||||
} else {
|
||||
@@ -215,7 +215,7 @@ func CreateOauth2Application(ctx *context.APIContext) {
|
||||
|
||||
app, err := auth.CreateOAuth2Application(auth.CreateOAuth2ApplicationOptions{
|
||||
Name: data.Name,
|
||||
UserID: ctx.User.ID,
|
||||
UserID: ctx.Doer.ID,
|
||||
RedirectURIs: data.RedirectURIs,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -252,7 +252,7 @@ func ListOauth2Applications(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/OAuth2ApplicationList"
|
||||
|
||||
apps, total, err := auth.ListOAuth2Applications(ctx.User.ID, utils.GetListOptions(ctx))
|
||||
apps, total, err := auth.ListOAuth2Applications(ctx.Doer.ID, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ListOAuth2Applications", err)
|
||||
return
|
||||
@@ -288,7 +288,7 @@ func DeleteOauth2Application(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
appID := ctx.ParamsInt64(":id")
|
||||
if err := auth.DeleteOAuth2Application(appID, ctx.User.ID); err != nil {
|
||||
if err := auth.DeleteOAuth2Application(appID, ctx.Doer.ID); err != nil {
|
||||
if auth.IsErrOAuthApplicationNotFound(err) {
|
||||
ctx.NotFound()
|
||||
} else {
|
||||
@@ -365,7 +365,7 @@ func UpdateOauth2Application(ctx *context.APIContext) {
|
||||
|
||||
app, err := auth.UpdateOAuth2Application(auth.UpdateOAuth2ApplicationOptions{
|
||||
Name: data.Name,
|
||||
UserID: ctx.User.ID,
|
||||
UserID: ctx.Doer.ID,
|
||||
ID: appID,
|
||||
RedirectURIs: data.RedirectURIs,
|
||||
})
|
||||
|
||||
@@ -28,7 +28,7 @@ func ListEmails(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/EmailList"
|
||||
|
||||
emails, err := user_model.GetEmailAddresses(ctx.User.ID)
|
||||
emails, err := user_model.GetEmailAddresses(ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetEmailAddresses", err)
|
||||
return
|
||||
@@ -71,7 +71,7 @@ func AddEmail(ctx *context.APIContext) {
|
||||
emails := make([]*user_model.EmailAddress, len(form.Emails))
|
||||
for i := range form.Emails {
|
||||
emails[i] = &user_model.EmailAddress{
|
||||
UID: ctx.User.ID,
|
||||
UID: ctx.Doer.ID,
|
||||
Email: form.Emails[i],
|
||||
IsActivated: !setting.Service.RegisterEmailConfirm,
|
||||
}
|
||||
@@ -80,8 +80,16 @@ func AddEmail(ctx *context.APIContext) {
|
||||
if err := user_model.AddEmailAddresses(emails); err != nil {
|
||||
if user_model.IsErrEmailAlreadyUsed(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", "Email address has been used: "+err.(user_model.ErrEmailAlreadyUsed).Email)
|
||||
} else if user_model.IsErrEmailInvalid(err) {
|
||||
errMsg := fmt.Sprintf("Email address %s invalid", err.(user_model.ErrEmailInvalid).Email)
|
||||
} else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) {
|
||||
email := ""
|
||||
if typedError, ok := err.(user_model.ErrEmailInvalid); ok {
|
||||
email = typedError.Email
|
||||
}
|
||||
if typedError, ok := err.(user_model.ErrEmailCharIsNotSupported); ok {
|
||||
email = typedError.Email
|
||||
}
|
||||
|
||||
errMsg := fmt.Sprintf("Email address %q invalid", email)
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", errMsg)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "AddEmailAddresses", err)
|
||||
@@ -123,7 +131,7 @@ func DeleteEmail(ctx *context.APIContext) {
|
||||
for i := range form.Emails {
|
||||
emails[i] = &user_model.EmailAddress{
|
||||
Email: form.Emails[i],
|
||||
UID: ctx.User.ID,
|
||||
UID: ctx.Doer.ID,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
func responseAPIUsers(ctx *context.APIContext, users []*user_model.User) {
|
||||
apiUsers := make([]*api.User, len(users))
|
||||
for i := range users {
|
||||
apiUsers[i] = convert.ToUser(users[i], ctx.User)
|
||||
apiUsers[i] = convert.ToUser(users[i], ctx.Doer)
|
||||
}
|
||||
ctx.JSON(http.StatusOK, &apiUsers)
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func ListMyFollowers(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
|
||||
listUserFollowers(ctx, ctx.User)
|
||||
listUserFollowers(ctx, ctx.Doer)
|
||||
}
|
||||
|
||||
// ListFollowers list the given user's followers
|
||||
@@ -82,11 +82,7 @@ func ListFollowers(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
|
||||
u := GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
listUserFollowers(ctx, u)
|
||||
listUserFollowers(ctx, ctx.ContextUser)
|
||||
}
|
||||
|
||||
func listUserFollowing(ctx *context.APIContext, u *user_model.User) {
|
||||
@@ -120,7 +116,7 @@ func ListMyFollowing(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
|
||||
listUserFollowing(ctx, ctx.User)
|
||||
listUserFollowing(ctx, ctx.Doer)
|
||||
}
|
||||
|
||||
// ListFollowing list the users that the given user is following
|
||||
@@ -148,11 +144,7 @@ func ListFollowing(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
|
||||
u := GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
listUserFollowing(ctx, u)
|
||||
listUserFollowing(ctx, ctx.ContextUser)
|
||||
}
|
||||
|
||||
func checkUserFollowing(ctx *context.APIContext, u *user_model.User, followID int64) {
|
||||
@@ -180,25 +172,21 @@ func CheckMyFollowing(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
target := GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
checkUserFollowing(ctx, ctx.User, target.ID)
|
||||
checkUserFollowing(ctx, ctx.Doer, ctx.ContextUser.ID)
|
||||
}
|
||||
|
||||
// CheckFollowing check if one user is following another user
|
||||
func CheckFollowing(ctx *context.APIContext) {
|
||||
// swagger:operation GET /users/{follower}/following/{followee} user userCheckFollowing
|
||||
// swagger:operation GET /users/{username}/following/{target} user userCheckFollowing
|
||||
// ---
|
||||
// summary: Check if one user is following another user
|
||||
// parameters:
|
||||
// - name: follower
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of following user
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: followee
|
||||
// - name: target
|
||||
// in: path
|
||||
// description: username of followed user
|
||||
// type: string
|
||||
@@ -209,15 +197,11 @@ func CheckFollowing(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
u := GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
target := GetUserByParamsName(ctx, ":target")
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
checkUserFollowing(ctx, u, target.ID)
|
||||
checkUserFollowing(ctx, ctx.ContextUser, target.ID)
|
||||
}
|
||||
|
||||
// Follow follow a user
|
||||
@@ -235,11 +219,7 @@ func Follow(ctx *context.APIContext) {
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
|
||||
target := GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if err := user_model.FollowUser(ctx.User.ID, target.ID); err != nil {
|
||||
if err := user_model.FollowUser(ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "FollowUser", err)
|
||||
return
|
||||
}
|
||||
@@ -261,11 +241,7 @@ func Unfollow(ctx *context.APIContext) {
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
|
||||
target := GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if err := user_model.UnfollowUser(ctx.User.ID, target.ID); err != nil {
|
||||
if err := user_model.UnfollowUser(ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UnfollowUser", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
func listGPGKeys(ctx *context.APIContext, uid int64, listOptions db.ListOptions) {
|
||||
keys, err := asymkey_model.ListGPGKeys(db.DefaultContext, uid, listOptions)
|
||||
keys, err := asymkey_model.ListGPGKeys(ctx, uid, listOptions)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ListGPGKeys", err)
|
||||
return
|
||||
@@ -64,11 +64,7 @@ func ListGPGKeys(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/GPGKeyList"
|
||||
|
||||
user := GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
listGPGKeys(ctx, user.ID, utils.GetListOptions(ctx))
|
||||
listGPGKeys(ctx, ctx.ContextUser.ID, utils.GetListOptions(ctx))
|
||||
}
|
||||
|
||||
// ListMyGPGKeys get the GPG key list of the authenticated user
|
||||
@@ -91,7 +87,7 @@ func ListMyGPGKeys(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/GPGKeyList"
|
||||
|
||||
listGPGKeys(ctx, ctx.User.ID, utils.GetListOptions(ctx))
|
||||
listGPGKeys(ctx, ctx.Doer.ID, utils.GetListOptions(ctx))
|
||||
}
|
||||
|
||||
// GetGPGKey get the GPG key based on a id
|
||||
@@ -128,8 +124,8 @@ func GetGPGKey(ctx *context.APIContext) {
|
||||
|
||||
// CreateUserGPGKey creates new GPG key to given user by ID.
|
||||
func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) {
|
||||
token := asymkey_model.VerificationToken(ctx.User, 1)
|
||||
lastToken := asymkey_model.VerificationToken(ctx.User, 0)
|
||||
token := asymkey_model.VerificationToken(ctx.Doer, 1)
|
||||
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
|
||||
|
||||
keys, err := asymkey_model.AddGPGKey(uid, form.ArmoredKey, token, form.Signature)
|
||||
if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) {
|
||||
@@ -156,7 +152,7 @@ func GetVerificationToken(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
token := asymkey_model.VerificationToken(ctx.User, 1)
|
||||
token := asymkey_model.VerificationToken(ctx.Doer, 1)
|
||||
ctx.PlainText(http.StatusOK, token)
|
||||
}
|
||||
|
||||
@@ -178,12 +174,12 @@ func VerifyUserGPGKey(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.VerifyGPGKeyOption)
|
||||
token := asymkey_model.VerificationToken(ctx.User, 1)
|
||||
lastToken := asymkey_model.VerificationToken(ctx.User, 0)
|
||||
token := asymkey_model.VerificationToken(ctx.Doer, 1)
|
||||
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
|
||||
|
||||
_, err := asymkey_model.VerifyGPGKey(ctx.User.ID, form.KeyID, token, form.Signature)
|
||||
_, err := asymkey_model.VerifyGPGKey(ctx.Doer.ID, form.KeyID, token, form.Signature)
|
||||
if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) {
|
||||
_, err = asymkey_model.VerifyGPGKey(ctx.User.ID, form.KeyID, lastToken, form.Signature)
|
||||
_, err = asymkey_model.VerifyGPGKey(ctx.Doer.ID, form.KeyID, lastToken, form.Signature)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -230,7 +226,7 @@ func CreateGPGKey(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateGPGKeyOption)
|
||||
CreateUserGPGKey(ctx, *form, ctx.User.ID)
|
||||
CreateUserGPGKey(ctx, *form, ctx.Doer.ID)
|
||||
}
|
||||
|
||||
// DeleteGPGKey remove a GPG key belonging to the authenticated user
|
||||
@@ -255,7 +251,7 @@ func DeleteGPGKey(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if err := asymkey_model.DeleteGPGKey(ctx.User, ctx.ParamsInt64(":id")); err != nil {
|
||||
if err := asymkey_model.DeleteGPGKey(ctx.Doer, ctx.ParamsInt64(":id")); err != nil {
|
||||
if asymkey_model.IsErrGPGKeyAccessDenied(err) {
|
||||
ctx.Error(http.StatusForbidden, "", "You do not have access to this key")
|
||||
} else {
|
||||
|
||||
@@ -86,7 +86,7 @@ func listPublicKeys(ctx *context.APIContext, user *user_model.User) {
|
||||
apiKeys := make([]*api.PublicKey, len(keys))
|
||||
for i := range keys {
|
||||
apiKeys[i] = convert.ToPublicKey(apiLink, keys[i])
|
||||
if ctx.User.IsAdmin || ctx.User.ID == keys[i].OwnerID {
|
||||
if ctx.Doer.IsAdmin || ctx.Doer.ID == keys[i].OwnerID {
|
||||
apiKeys[i], _ = appendPrivateInformation(apiKeys[i], keys[i], user)
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ func ListMyPublicKeys(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/PublicKeyList"
|
||||
|
||||
listPublicKeys(ctx, ctx.User)
|
||||
listPublicKeys(ctx, ctx.Doer)
|
||||
}
|
||||
|
||||
// ListPublicKeys list the given user's public keys
|
||||
@@ -151,11 +151,7 @@ func ListPublicKeys(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/PublicKeyList"
|
||||
|
||||
user := GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
listPublicKeys(ctx, user)
|
||||
listPublicKeys(ctx, ctx.ContextUser)
|
||||
}
|
||||
|
||||
// GetPublicKey get a public key
|
||||
@@ -190,8 +186,8 @@ func GetPublicKey(ctx *context.APIContext) {
|
||||
|
||||
apiLink := composePublicKeysAPILink()
|
||||
apiKey := convert.ToPublicKey(apiLink, key)
|
||||
if ctx.User.IsAdmin || ctx.User.ID == key.OwnerID {
|
||||
apiKey, _ = appendPrivateInformation(apiKey, key, ctx.User)
|
||||
if ctx.Doer.IsAdmin || ctx.Doer.ID == key.OwnerID {
|
||||
apiKey, _ = appendPrivateInformation(apiKey, key, ctx.Doer)
|
||||
}
|
||||
ctx.JSON(http.StatusOK, apiKey)
|
||||
}
|
||||
@@ -211,8 +207,8 @@ func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid
|
||||
}
|
||||
apiLink := composePublicKeysAPILink()
|
||||
apiKey := convert.ToPublicKey(apiLink, key)
|
||||
if ctx.User.IsAdmin || ctx.User.ID == key.OwnerID {
|
||||
apiKey, _ = appendPrivateInformation(apiKey, key, ctx.User)
|
||||
if ctx.Doer.IsAdmin || ctx.Doer.ID == key.OwnerID {
|
||||
apiKey, _ = appendPrivateInformation(apiKey, key, ctx.Doer)
|
||||
}
|
||||
ctx.JSON(http.StatusCreated, apiKey)
|
||||
}
|
||||
@@ -238,7 +234,7 @@ func CreatePublicKey(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateKeyOption)
|
||||
CreateUserPublicKey(ctx, *form, ctx.User.ID)
|
||||
CreateUserPublicKey(ctx, *form, ctx.Doer.ID)
|
||||
}
|
||||
|
||||
// DeletePublicKey delete one public key
|
||||
@@ -266,16 +262,21 @@ func DeletePublicKey(ctx *context.APIContext) {
|
||||
id := ctx.ParamsInt64(":id")
|
||||
externallyManaged, err := asymkey_model.PublicKeyIsExternallyManaged(id)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "PublicKeyIsExternallyManaged", err)
|
||||
}
|
||||
if externallyManaged {
|
||||
ctx.Error(http.StatusForbidden, "", "SSH Key is externally managed for this user")
|
||||
}
|
||||
|
||||
if err := asymkey_service.DeletePublicKey(ctx.User, id); err != nil {
|
||||
if asymkey_model.IsErrKeyNotExist(err) {
|
||||
ctx.NotFound()
|
||||
} else if asymkey_model.IsErrKeyAccessDenied(err) {
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "PublicKeyIsExternallyManaged", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if externallyManaged {
|
||||
ctx.Error(http.StatusForbidden, "", "SSH Key is externally managed for this user")
|
||||
return
|
||||
}
|
||||
|
||||
if err := asymkey_service.DeletePublicKey(ctx.Doer, id); err != nil {
|
||||
if asymkey_model.IsErrKeyAccessDenied(err) {
|
||||
ctx.Error(http.StatusForbidden, "", "You do not have access to this key")
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "DeletePublicKey", err)
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
@@ -39,12 +38,12 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
|
||||
|
||||
apiRepos := make([]*api.Repository, 0, len(repos))
|
||||
for i := range repos {
|
||||
access, err := models.AccessLevel(ctx.User, repos[i])
|
||||
access, err := models.AccessLevel(ctx.Doer, repos[i])
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "AccessLevel", err)
|
||||
return
|
||||
}
|
||||
if ctx.IsSigned && ctx.User.IsAdmin || access >= perm.AccessModeRead {
|
||||
if ctx.IsSigned && ctx.Doer.IsAdmin || access >= perm.AccessModeRead {
|
||||
apiRepos = append(apiRepos, convert.ToRepo(repos[i], access))
|
||||
}
|
||||
}
|
||||
@@ -79,12 +78,8 @@ func ListUserRepos(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/RepositoryList"
|
||||
|
||||
user := GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
private := ctx.IsSigned
|
||||
listUserRepos(ctx, user, private)
|
||||
listUserRepos(ctx, ctx.ContextUser, private)
|
||||
}
|
||||
|
||||
// ListMyRepos - list the repositories you own or have access to.
|
||||
@@ -109,8 +104,8 @@ func ListMyRepos(ctx *context.APIContext) {
|
||||
|
||||
opts := &models.SearchRepoOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
Actor: ctx.User,
|
||||
OwnerID: ctx.User.ID,
|
||||
Actor: ctx.Doer,
|
||||
OwnerID: ctx.Doer.ID,
|
||||
Private: ctx.IsSigned,
|
||||
IncludeDescription: true,
|
||||
}
|
||||
@@ -124,11 +119,11 @@ func ListMyRepos(ctx *context.APIContext) {
|
||||
|
||||
results := make([]*api.Repository, len(repos))
|
||||
for i, repo := range repos {
|
||||
if err = repo.GetOwner(db.DefaultContext); err != nil {
|
||||
if err = repo.GetOwner(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetOwner", err)
|
||||
return
|
||||
}
|
||||
accessMode, err := models.AccessLevel(ctx.User, repo)
|
||||
accessMode, err := models.AccessLevel(ctx.Doer, repo)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "AccessLevel", err)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func GetUserSettings(ctx *context.APIContext) {
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserSettings"
|
||||
ctx.JSON(http.StatusOK, convert.User2UserSettings(ctx.User))
|
||||
ctx.JSON(http.StatusOK, convert.User2UserSettings(ctx.Doer))
|
||||
}
|
||||
|
||||
// UpdateUserSettings returns user settings
|
||||
@@ -46,38 +46,38 @@ func UpdateUserSettings(ctx *context.APIContext) {
|
||||
form := web.GetForm(ctx).(*api.UserSettingsOptions)
|
||||
|
||||
if form.FullName != nil {
|
||||
ctx.User.FullName = *form.FullName
|
||||
ctx.Doer.FullName = *form.FullName
|
||||
}
|
||||
if form.Description != nil {
|
||||
ctx.User.Description = *form.Description
|
||||
ctx.Doer.Description = *form.Description
|
||||
}
|
||||
if form.Website != nil {
|
||||
ctx.User.Website = *form.Website
|
||||
ctx.Doer.Website = *form.Website
|
||||
}
|
||||
if form.Location != nil {
|
||||
ctx.User.Location = *form.Location
|
||||
ctx.Doer.Location = *form.Location
|
||||
}
|
||||
if form.Language != nil {
|
||||
ctx.User.Language = *form.Language
|
||||
ctx.Doer.Language = *form.Language
|
||||
}
|
||||
if form.Theme != nil {
|
||||
ctx.User.Theme = *form.Theme
|
||||
ctx.Doer.Theme = *form.Theme
|
||||
}
|
||||
if form.DiffViewStyle != nil {
|
||||
ctx.User.DiffViewStyle = *form.DiffViewStyle
|
||||
ctx.Doer.DiffViewStyle = *form.DiffViewStyle
|
||||
}
|
||||
|
||||
if form.HideEmail != nil {
|
||||
ctx.User.KeepEmailPrivate = *form.HideEmail
|
||||
ctx.Doer.KeepEmailPrivate = *form.HideEmail
|
||||
}
|
||||
if form.HideActivity != nil {
|
||||
ctx.User.KeepActivityPrivate = *form.HideActivity
|
||||
ctx.Doer.KeepActivityPrivate = *form.HideActivity
|
||||
}
|
||||
|
||||
if err := user_model.UpdateUser(ctx.User, false); err != nil {
|
||||
if err := user_model.UpdateUser(ctx.Doer, false); err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.User2UserSettings(ctx.User))
|
||||
ctx.JSON(http.StatusOK, convert.User2UserSettings(ctx.Doer))
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// getStarredRepos returns the repos that the user with the specified userID has
|
||||
// starred
|
||||
func getStarredRepos(user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) {
|
||||
starredRepos, err := models.GetStarredRepos(user.ID, private, listOptions)
|
||||
starredRepos, err := repo_model.GetStarredRepos(user.ID, private, listOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -62,15 +62,14 @@ func GetStarredRepos(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/RepositoryList"
|
||||
|
||||
user := GetUserByParams(ctx)
|
||||
private := user.ID == ctx.User.ID
|
||||
repos, err := getStarredRepos(user, private, utils.GetListOptions(ctx))
|
||||
private := ctx.ContextUser.ID == ctx.Doer.ID
|
||||
repos, err := getStarredRepos(ctx.ContextUser, private, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(int64(user.NumStars))
|
||||
ctx.SetTotalCountHeader(int64(ctx.ContextUser.NumStars))
|
||||
ctx.JSON(http.StatusOK, &repos)
|
||||
}
|
||||
|
||||
@@ -94,12 +93,12 @@ func GetMyStarredRepos(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/RepositoryList"
|
||||
|
||||
repos, err := getStarredRepos(ctx.User, true, utils.GetListOptions(ctx))
|
||||
repos, err := getStarredRepos(ctx.Doer, true, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(int64(ctx.User.NumStars))
|
||||
ctx.SetTotalCountHeader(int64(ctx.Doer.NumStars))
|
||||
ctx.JSON(http.StatusOK, &repos)
|
||||
}
|
||||
|
||||
@@ -125,7 +124,7 @@ func IsStarring(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if repo_model.IsStaring(ctx.User.ID, ctx.Repo.Repository.ID) {
|
||||
if repo_model.IsStaring(ctx.Doer.ID, ctx.Repo.Repository.ID) {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
} else {
|
||||
ctx.NotFound()
|
||||
@@ -152,7 +151,7 @@ func Star(ctx *context.APIContext) {
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
|
||||
err := repo_model.StarRepo(ctx.User.ID, ctx.Repo.Repository.ID, true)
|
||||
err := repo_model.StarRepo(ctx.Doer.ID, ctx.Repo.Repository.ID, true)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "StarRepo", err)
|
||||
return
|
||||
@@ -180,7 +179,7 @@ func Unstar(ctx *context.APIContext) {
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
|
||||
err := repo_model.StarRepo(ctx.User.ID, ctx.Repo.Repository.ID, false)
|
||||
err := repo_model.StarRepo(ctx.Doer.ID, ctx.Repo.Repository.ID, false)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "StarRepo", err)
|
||||
return
|
||||
|
||||
@@ -56,7 +56,7 @@ func Search(ctx *context.APIContext) {
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
users, maxResults, err := user_model.SearchUsers(&user_model.SearchUserOptions{
|
||||
Actor: ctx.User,
|
||||
Actor: ctx.Doer,
|
||||
Keyword: ctx.FormTrim("q"),
|
||||
UID: ctx.FormInt64("uid"),
|
||||
Type: user_model.UserTypeIndividual,
|
||||
@@ -75,7 +75,7 @@ func Search(ctx *context.APIContext) {
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]interface{}{
|
||||
"ok": true,
|
||||
"data": convert.ToUsers(ctx.User, users),
|
||||
"data": convert.ToUsers(ctx.Doer, users),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -98,18 +98,12 @@ func GetInfo(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
u := GetUserByParams(ctx)
|
||||
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if !models.IsUserVisibleToViewer(u, ctx.User) {
|
||||
if !user_model.IsUserVisibleToViewer(ctx.ContextUser, ctx.Doer) {
|
||||
// fake ErrUserNotExist error message to not leak information about existence
|
||||
ctx.NotFound("GetUserByName", user_model.ErrUserNotExist{Name: ctx.Params(":username")})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToUser(u, ctx.User))
|
||||
ctx.JSON(http.StatusOK, convert.ToUser(ctx.ContextUser, ctx.Doer))
|
||||
}
|
||||
|
||||
// GetAuthenticatedUser get current user's information
|
||||
@@ -123,7 +117,7 @@ func GetAuthenticatedUser(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/User"
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToUser(ctx.User, ctx.User))
|
||||
ctx.JSON(http.StatusOK, convert.ToUser(ctx.Doer, ctx.Doer))
|
||||
}
|
||||
|
||||
// GetUserHeatmapData is the handler to get a users heatmap
|
||||
@@ -145,12 +139,7 @@ func GetUserHeatmapData(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
user := GetUserByParams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
heatmap, err := models.GetUserHeatmapDataByUser(user, ctx.User)
|
||||
heatmap, err := models.GetUserHeatmapDataByUser(ctx.ContextUser, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserHeatmapDataByUser", err)
|
||||
return
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
// getWatchedRepos returns the repos that the user with the specified userID is watching
|
||||
func getWatchedRepos(user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, int64, error) {
|
||||
watchedRepos, total, err := models.GetWatchedRepos(user.ID, private, listOptions)
|
||||
watchedRepos, total, err := repo_model.GetWatchedRepos(user.ID, private, listOptions)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -60,9 +60,8 @@ func GetWatchedRepos(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/RepositoryList"
|
||||
|
||||
user := GetUserByParams(ctx)
|
||||
private := user.ID == ctx.User.ID
|
||||
repos, total, err := getWatchedRepos(user, private, utils.GetListOptions(ctx))
|
||||
private := ctx.ContextUser.ID == ctx.Doer.ID
|
||||
repos, total, err := getWatchedRepos(ctx.ContextUser, private, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
|
||||
}
|
||||
@@ -91,7 +90,7 @@ func GetMyWatchedRepos(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/RepositoryList"
|
||||
|
||||
repos, total, err := getWatchedRepos(ctx.User, true, utils.GetListOptions(ctx))
|
||||
repos, total, err := getWatchedRepos(ctx.Doer, true, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
|
||||
}
|
||||
@@ -123,7 +122,7 @@ func IsWatching(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// description: User is not watching this repo or repo do not exist
|
||||
|
||||
if repo_model.IsWatching(ctx.User.ID, ctx.Repo.Repository.ID) {
|
||||
if repo_model.IsWatching(ctx.Doer.ID, ctx.Repo.Repository.ID) {
|
||||
ctx.JSON(http.StatusOK, api.WatchInfo{
|
||||
Subscribed: true,
|
||||
Ignored: false,
|
||||
@@ -157,7 +156,7 @@ func Watch(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/WatchInfo"
|
||||
|
||||
err := repo_model.WatchRepo(ctx.User.ID, ctx.Repo.Repository.ID, true)
|
||||
err := repo_model.WatchRepo(ctx.Doer.ID, ctx.Repo.Repository.ID, true)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "WatchRepo", err)
|
||||
return
|
||||
@@ -192,7 +191,7 @@ func Unwatch(ctx *context.APIContext) {
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
|
||||
err := repo_model.WatchRepo(ctx.User.ID, ctx.Repo.Repository.ID, false)
|
||||
err := repo_model.WatchRepo(ctx.Doer.ID, ctx.Repo.Repository.ID, false)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UnwatchRepo", err)
|
||||
return
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
@@ -35,12 +36,7 @@ func ResolveRefOrSha(ctx *context.APIContext, ref string) string {
|
||||
// GetGitRefs return git references based on filter
|
||||
func GetGitRefs(ctx *context.APIContext, filter string) ([]*git.Reference, string, error) {
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
var err error
|
||||
ctx.Repo.GitRepo, err = git.OpenRepositoryCtx(ctx, ctx.Repo.Repository.RepoPath())
|
||||
if err != nil {
|
||||
return nil, "OpenRepository", err
|
||||
}
|
||||
defer ctx.Repo.GitRepo.Close()
|
||||
return nil, "", fmt.Errorf("no open git repo found in context")
|
||||
}
|
||||
if len(filter) > 0 {
|
||||
filter = "refs/" + filter
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
@@ -164,7 +163,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, orgID, repoID
|
||||
if err := w.UpdateEvent(); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateEvent", err)
|
||||
return nil, false
|
||||
} else if err := webhook.CreateWebhook(db.DefaultContext, w); err != nil {
|
||||
} else if err := webhook.CreateWebhook(ctx, w); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateWebhook", err)
|
||||
return nil, false
|
||||
}
|
||||
@@ -246,18 +245,29 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
|
||||
w.ChooseEvents = true
|
||||
w.Create = util.IsStringInSlice(string(webhook.HookEventCreate), form.Events, true)
|
||||
w.Push = util.IsStringInSlice(string(webhook.HookEventPush), form.Events, true)
|
||||
w.PullRequest = util.IsStringInSlice(string(webhook.HookEventPullRequest), form.Events, true)
|
||||
w.Create = util.IsStringInSlice(string(webhook.HookEventCreate), form.Events, true)
|
||||
w.Delete = util.IsStringInSlice(string(webhook.HookEventDelete), form.Events, true)
|
||||
w.Fork = util.IsStringInSlice(string(webhook.HookEventFork), form.Events, true)
|
||||
w.Issues = util.IsStringInSlice(string(webhook.HookEventIssues), form.Events, true)
|
||||
w.IssueComment = util.IsStringInSlice(string(webhook.HookEventIssueComment), form.Events, true)
|
||||
w.Push = util.IsStringInSlice(string(webhook.HookEventPush), form.Events, true)
|
||||
w.PullRequest = util.IsStringInSlice(string(webhook.HookEventPullRequest), form.Events, true)
|
||||
w.Repository = util.IsStringInSlice(string(webhook.HookEventRepository), form.Events, true)
|
||||
w.Release = util.IsStringInSlice(string(webhook.HookEventRelease), form.Events, true)
|
||||
w.BranchFilter = form.BranchFilter
|
||||
|
||||
// Issues
|
||||
w.Issues = issuesHook(form.Events, "issues_only")
|
||||
w.IssueAssign = issuesHook(form.Events, string(webhook.HookEventIssueAssign))
|
||||
w.IssueLabel = issuesHook(form.Events, string(webhook.HookEventIssueLabel))
|
||||
w.IssueMilestone = issuesHook(form.Events, string(webhook.HookEventIssueMilestone))
|
||||
w.IssueComment = issuesHook(form.Events, string(webhook.HookEventIssueComment))
|
||||
|
||||
// Pull requests
|
||||
w.PullRequest = pullHook(form.Events, "pull_request_only")
|
||||
w.PullRequestAssign = pullHook(form.Events, string(webhook.HookEventPullRequestAssign))
|
||||
w.PullRequestLabel = pullHook(form.Events, string(webhook.HookEventPullRequestLabel))
|
||||
w.PullRequestMilestone = pullHook(form.Events, string(webhook.HookEventPullRequestMilestone))
|
||||
w.PullRequestComment = pullHook(form.Events, string(webhook.HookEventPullRequestComment))
|
||||
w.PullRequestReview = pullHook(form.Events, "pull_request_review")
|
||||
w.PullRequestSync = pullHook(form.Events, string(webhook.HookEventPullRequestSync))
|
||||
|
||||
if err := w.UpdateEvent(); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateEvent", err)
|
||||
return false
|
||||
|
||||
19
routers/api/v1/utils/page.go
Normal file
19
routers/api/v1/utils/page.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// 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 utils
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
)
|
||||
|
||||
// GetListOptions returns list options using the page and limit parameters
|
||||
func GetListOptions(ctx *context.APIContext) db.ListOptions {
|
||||
return db.ListOptions{
|
||||
Page: ctx.FormInt("page"),
|
||||
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
// 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 utils
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/convert"
|
||||
)
|
||||
|
||||
// GetQueryBeforeSince return parsed time (unix format) from URL query's before and since
|
||||
func GetQueryBeforeSince(ctx *context.APIContext) (before, since int64, err error) {
|
||||
qCreatedBefore, err := prepareQueryArg(ctx, "before")
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
qCreatedSince, err := prepareQueryArg(ctx, "since")
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
before, err = parseTime(qCreatedBefore)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
since, err = parseTime(qCreatedSince)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return before, since, nil
|
||||
}
|
||||
|
||||
// parseTime parse time and return unix timestamp
|
||||
func parseTime(value string) (int64, error) {
|
||||
if len(value) != 0 {
|
||||
t, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !t.IsZero() {
|
||||
return t.Unix(), nil
|
||||
}
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// prepareQueryArg unescape and trim a query arg
|
||||
func prepareQueryArg(ctx *context.APIContext, name string) (value string, err error) {
|
||||
value, err = url.PathUnescape(ctx.FormString(name))
|
||||
value = strings.TrimSpace(value)
|
||||
return
|
||||
}
|
||||
|
||||
// GetListOptions returns list options using the page and limit parameters
|
||||
func GetListOptions(ctx *context.APIContext) db.ListOptions {
|
||||
return db.ListOptions{
|
||||
Page: ctx.FormInt("page"),
|
||||
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user