cdb088cec2
This PR adds a [CRAN](https://cran.r-project.org/) package registry. ![grafik](https://user-images.githubusercontent.com/1666336/210450039-d6fa6f77-20cd-4741-89a8-1624def267f7.png)
267 lines
6.9 KiB
Go
267 lines
6.9 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cran
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
packages_model "code.gitea.io/gitea/models/packages"
|
|
cran_model "code.gitea.io/gitea/models/packages/cran"
|
|
"code.gitea.io/gitea/modules/context"
|
|
packages_module "code.gitea.io/gitea/modules/packages"
|
|
cran_module "code.gitea.io/gitea/modules/packages/cran"
|
|
"code.gitea.io/gitea/modules/util"
|
|
"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)
|
|
})
|
|
}
|
|
|
|
func EnumerateSourcePackages(ctx *context.Context) {
|
|
enumeratePackages(ctx, ctx.Params("format"), &cran_model.SearchOptions{
|
|
OwnerID: ctx.Package.Owner.ID,
|
|
FileType: cran_module.TypeSource,
|
|
})
|
|
}
|
|
|
|
func EnumerateBinaryPackages(ctx *context.Context) {
|
|
enumeratePackages(ctx, ctx.Params("format"), &cran_model.SearchOptions{
|
|
OwnerID: ctx.Package.Owner.ID,
|
|
FileType: cran_module.TypeBinary,
|
|
Platform: ctx.Params("platform"),
|
|
RVersion: ctx.Params("rversion"),
|
|
})
|
|
}
|
|
|
|
func enumeratePackages(ctx *context.Context, format string, opts *cran_model.SearchOptions) {
|
|
if format != "" && format != ".gz" {
|
|
apiError(ctx, http.StatusNotFound, nil)
|
|
return
|
|
}
|
|
|
|
pvs, err := cran_model.SearchLatestVersions(ctx, opts)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
if len(pvs) == 0 {
|
|
apiError(ctx, http.StatusNotFound, nil)
|
|
return
|
|
}
|
|
|
|
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
var w io.Writer = ctx.Resp
|
|
|
|
if format == ".gz" {
|
|
ctx.Resp.Header().Set("Content-Type", "application/x-gzip")
|
|
|
|
gzw := gzip.NewWriter(w)
|
|
defer gzw.Close()
|
|
|
|
w = gzw
|
|
} else {
|
|
ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
|
|
}
|
|
ctx.Resp.WriteHeader(http.StatusOK)
|
|
|
|
for i, pd := range pds {
|
|
if i > 0 {
|
|
fmt.Fprintln(w)
|
|
}
|
|
|
|
var pfd *packages_model.PackageFileDescriptor
|
|
for _, d := range pd.Files {
|
|
if d.Properties.GetByName(cran_module.PropertyType) == opts.FileType &&
|
|
d.Properties.GetByName(cran_module.PropertyPlatform) == opts.Platform &&
|
|
d.Properties.GetByName(cran_module.PropertyRVersion) == opts.RVersion {
|
|
pfd = d
|
|
break
|
|
}
|
|
}
|
|
|
|
metadata := pd.Metadata.(*cran_module.Metadata)
|
|
|
|
fmt.Fprintln(w, "Package:", pd.Package.Name)
|
|
fmt.Fprintln(w, "Version:", pd.Version.Version)
|
|
if metadata.License != "" {
|
|
fmt.Fprintln(w, "License:", metadata.License)
|
|
}
|
|
if len(metadata.Depends) > 0 {
|
|
fmt.Fprintln(w, "Depends:", strings.Join(metadata.Depends, ", "))
|
|
}
|
|
if len(metadata.Imports) > 0 {
|
|
fmt.Fprintln(w, "Imports:", strings.Join(metadata.Imports, ", "))
|
|
}
|
|
if len(metadata.LinkingTo) > 0 {
|
|
fmt.Fprintln(w, "LinkingTo:", strings.Join(metadata.LinkingTo, ", "))
|
|
}
|
|
if len(metadata.Suggests) > 0 {
|
|
fmt.Fprintln(w, "Suggests:", strings.Join(metadata.Suggests, ", "))
|
|
}
|
|
needsCompilation := "no"
|
|
if metadata.NeedsCompilation {
|
|
needsCompilation = "yes"
|
|
}
|
|
fmt.Fprintln(w, "NeedsCompilation:", needsCompilation)
|
|
fmt.Fprintln(w, "MD5sum:", pfd.Blob.HashMD5)
|
|
}
|
|
}
|
|
|
|
func UploadSourcePackageFile(ctx *context.Context) {
|
|
uploadPackageFile(
|
|
ctx,
|
|
packages_model.EmptyFileKey,
|
|
map[string]string{
|
|
cran_module.PropertyType: cran_module.TypeSource,
|
|
},
|
|
)
|
|
}
|
|
|
|
func UploadBinaryPackageFile(ctx *context.Context) {
|
|
platform, rversion := ctx.FormTrim("platform"), ctx.FormTrim("rversion")
|
|
if platform == "" || rversion == "" {
|
|
apiError(ctx, http.StatusBadRequest, nil)
|
|
return
|
|
}
|
|
|
|
uploadPackageFile(
|
|
ctx,
|
|
platform+"|"+rversion,
|
|
map[string]string{
|
|
cran_module.PropertyType: cran_module.TypeBinary,
|
|
cran_module.PropertyPlatform: platform,
|
|
cran_module.PropertyRVersion: rversion,
|
|
},
|
|
)
|
|
}
|
|
|
|
func uploadPackageFile(ctx *context.Context, compositeKey string, properties map[string]string) {
|
|
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)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
defer buf.Close()
|
|
|
|
pck, err := cran_module.ParsePackage(buf, buf.Size())
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrInvalidArgument) {
|
|
apiError(ctx, http.StatusBadRequest, err)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
|
|
&packages_service.PackageCreationInfo{
|
|
PackageInfo: packages_service.PackageInfo{
|
|
Owner: ctx.Package.Owner,
|
|
PackageType: packages_model.TypeCran,
|
|
Name: pck.Name,
|
|
Version: pck.Version,
|
|
},
|
|
SemverCompatible: false,
|
|
Creator: ctx.Doer,
|
|
Metadata: pck.Metadata,
|
|
},
|
|
&packages_service.PackageFileCreationInfo{
|
|
PackageFileInfo: packages_service.PackageFileInfo{
|
|
Filename: fmt.Sprintf("%s_%s%s", pck.Name, pck.Version, pck.FileExtension),
|
|
CompositeKey: compositeKey,
|
|
},
|
|
Creator: ctx.Doer,
|
|
Data: buf,
|
|
IsLead: true,
|
|
Properties: properties,
|
|
},
|
|
)
|
|
if err != nil {
|
|
switch err {
|
|
case packages_model.ErrDuplicatePackageFile:
|
|
apiError(ctx, http.StatusConflict, err)
|
|
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
|
apiError(ctx, http.StatusForbidden, err)
|
|
default:
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
ctx.Status(http.StatusCreated)
|
|
}
|
|
|
|
func DownloadSourcePackageFile(ctx *context.Context) {
|
|
downloadPackageFile(ctx, &cran_model.SearchOptions{
|
|
OwnerID: ctx.Package.Owner.ID,
|
|
FileType: cran_module.TypeSource,
|
|
Filename: ctx.Params("filename"),
|
|
})
|
|
}
|
|
|
|
func DownloadBinaryPackageFile(ctx *context.Context) {
|
|
downloadPackageFile(ctx, &cran_model.SearchOptions{
|
|
OwnerID: ctx.Package.Owner.ID,
|
|
FileType: cran_module.TypeBinary,
|
|
Platform: ctx.Params("platform"),
|
|
RVersion: ctx.Params("rversion"),
|
|
Filename: ctx.Params("filename"),
|
|
})
|
|
}
|
|
|
|
func downloadPackageFile(ctx *context.Context, opts *cran_model.SearchOptions) {
|
|
pf, err := cran_model.SearchFile(ctx, opts)
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
s, _, err := packages_service.GetPackageFileStream(ctx, pf)
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
defer s.Close()
|
|
|
|
ctx.ServeContent(s, &context.ServeHeaderOptions{
|
|
Filename: pf.Name,
|
|
LastModified: pf.CreatedUnix.AsLocalTime(),
|
|
})
|
|
}
|