code.gitea.io/gitea@v1.21.7/routers/api/packages/cargo/cargo.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package cargo
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"net/http"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"code.gitea.io/gitea/models/db"
    14  	packages_model "code.gitea.io/gitea/models/packages"
    15  	"code.gitea.io/gitea/modules/context"
    16  	"code.gitea.io/gitea/modules/log"
    17  	packages_module "code.gitea.io/gitea/modules/packages"
    18  	cargo_module "code.gitea.io/gitea/modules/packages/cargo"
    19  	"code.gitea.io/gitea/modules/setting"
    20  	"code.gitea.io/gitea/modules/structs"
    21  	"code.gitea.io/gitea/modules/util"
    22  	"code.gitea.io/gitea/routers/api/packages/helper"
    23  	"code.gitea.io/gitea/services/convert"
    24  	packages_service "code.gitea.io/gitea/services/packages"
    25  	cargo_service "code.gitea.io/gitea/services/packages/cargo"
    26  )
    27  
    28  // https://doc.rust-lang.org/cargo/reference/registries.html#web-api
    29  type StatusResponse struct {
    30  	OK     bool            `json:"ok"`
    31  	Errors []StatusMessage `json:"errors,omitempty"`
    32  }
    33  
    34  type StatusMessage struct {
    35  	Message string `json:"detail"`
    36  }
    37  
    38  func apiError(ctx *context.Context, status int, obj any) {
    39  	helper.LogAndProcessError(ctx, status, obj, func(message string) {
    40  		ctx.JSON(status, StatusResponse{
    41  			OK: false,
    42  			Errors: []StatusMessage{
    43  				{
    44  					Message: message,
    45  				},
    46  			},
    47  		})
    48  	})
    49  }
    50  
    51  // https://rust-lang.github.io/rfcs/2789-sparse-index.html
    52  func RepositoryConfig(ctx *context.Context) {
    53  	ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInView || ctx.Package.Owner.Visibility != structs.VisibleTypePublic))
    54  }
    55  
    56  func EnumeratePackageVersions(ctx *context.Context) {
    57  	p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeCargo, ctx.Params("package"))
    58  	if err != nil {
    59  		if errors.Is(err, util.ErrNotExist) {
    60  			apiError(ctx, http.StatusNotFound, err)
    61  		} else {
    62  			apiError(ctx, http.StatusInternalServerError, err)
    63  		}
    64  		return
    65  	}
    66  
    67  	b, err := cargo_service.BuildPackageIndex(ctx, p)
    68  	if err != nil {
    69  		apiError(ctx, http.StatusInternalServerError, err)
    70  		return
    71  	}
    72  	if b == nil {
    73  		apiError(ctx, http.StatusNotFound, nil)
    74  		return
    75  	}
    76  
    77  	ctx.PlainTextBytes(http.StatusOK, b.Bytes())
    78  }
    79  
    80  type SearchResult struct {
    81  	Crates []*SearchResultCrate `json:"crates"`
    82  	Meta   SearchResultMeta     `json:"meta"`
    83  }
    84  
    85  type SearchResultCrate struct {
    86  	Name          string `json:"name"`
    87  	LatestVersion string `json:"max_version"`
    88  	Description   string `json:"description"`
    89  }
    90  
    91  type SearchResultMeta struct {
    92  	Total int64 `json:"total"`
    93  }
    94  
    95  // https://doc.rust-lang.org/cargo/reference/registries.html#search
    96  func SearchPackages(ctx *context.Context) {
    97  	page := ctx.FormInt("page")
    98  	if page < 1 {
    99  		page = 1
   100  	}
   101  	perPage := ctx.FormInt("per_page")
   102  	paginator := db.ListOptions{
   103  		Page:     page,
   104  		PageSize: convert.ToCorrectPageSize(perPage),
   105  	}
   106  
   107  	pvs, total, err := packages_model.SearchLatestVersions(
   108  		ctx,
   109  		&packages_model.PackageSearchOptions{
   110  			OwnerID:    ctx.Package.Owner.ID,
   111  			Type:       packages_model.TypeCargo,
   112  			Name:       packages_model.SearchValue{Value: ctx.FormTrim("q")},
   113  			IsInternal: util.OptionalBoolFalse,
   114  			Paginator:  &paginator,
   115  		},
   116  	)
   117  	if err != nil {
   118  		apiError(ctx, http.StatusInternalServerError, err)
   119  		return
   120  	}
   121  
   122  	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
   123  	if err != nil {
   124  		apiError(ctx, http.StatusInternalServerError, err)
   125  		return
   126  	}
   127  
   128  	crates := make([]*SearchResultCrate, 0, len(pvs))
   129  	for _, pd := range pds {
   130  		crates = append(crates, &SearchResultCrate{
   131  			Name:          pd.Package.Name,
   132  			LatestVersion: pd.Version.Version,
   133  			Description:   pd.Metadata.(*cargo_module.Metadata).Description,
   134  		})
   135  	}
   136  
   137  	ctx.JSON(http.StatusOK, SearchResult{
   138  		Crates: crates,
   139  		Meta: SearchResultMeta{
   140  			Total: total,
   141  		},
   142  	})
   143  }
   144  
   145  type Owners struct {
   146  	Users []OwnerUser `json:"users"`
   147  }
   148  
   149  type OwnerUser struct {
   150  	ID    int64  `json:"id"`
   151  	Login string `json:"login"`
   152  	Name  string `json:"name"`
   153  }
   154  
   155  // https://doc.rust-lang.org/cargo/reference/registries.html#owners-list
   156  func ListOwners(ctx *context.Context) {
   157  	ctx.JSON(http.StatusOK, Owners{
   158  		Users: []OwnerUser{
   159  			{
   160  				ID:    ctx.Package.Owner.ID,
   161  				Login: ctx.Package.Owner.Name,
   162  				Name:  ctx.Package.Owner.DisplayName(),
   163  			},
   164  		},
   165  	})
   166  }
   167  
   168  // DownloadPackageFile serves the content of a package
   169  func DownloadPackageFile(ctx *context.Context) {
   170  	s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
   171  		ctx,
   172  		&packages_service.PackageInfo{
   173  			Owner:       ctx.Package.Owner,
   174  			PackageType: packages_model.TypeCargo,
   175  			Name:        ctx.Params("package"),
   176  			Version:     ctx.Params("version"),
   177  		},
   178  		&packages_service.PackageFileInfo{
   179  			Filename: strings.ToLower(fmt.Sprintf("%s-%s.crate", ctx.Params("package"), ctx.Params("version"))),
   180  		},
   181  	)
   182  	if err != nil {
   183  		if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
   184  			apiError(ctx, http.StatusNotFound, err)
   185  			return
   186  		}
   187  		apiError(ctx, http.StatusInternalServerError, err)
   188  		return
   189  	}
   190  
   191  	helper.ServePackageFile(ctx, s, u, pf)
   192  }
   193  
   194  // https://doc.rust-lang.org/cargo/reference/registries.html#publish
   195  func UploadPackage(ctx *context.Context) {
   196  	defer ctx.Req.Body.Close()
   197  
   198  	cp, err := cargo_module.ParsePackage(ctx.Req.Body)
   199  	if err != nil {
   200  		apiError(ctx, http.StatusBadRequest, err)
   201  		return
   202  	}
   203  
   204  	buf, err := packages_module.CreateHashedBufferFromReader(cp.Content)
   205  	if err != nil {
   206  		apiError(ctx, http.StatusInternalServerError, err)
   207  		return
   208  	}
   209  	defer buf.Close()
   210  
   211  	if buf.Size() != cp.ContentSize {
   212  		apiError(ctx, http.StatusBadRequest, "invalid content size")
   213  		return
   214  	}
   215  
   216  	pv, _, err := packages_service.CreatePackageAndAddFile(
   217  		ctx,
   218  		&packages_service.PackageCreationInfo{
   219  			PackageInfo: packages_service.PackageInfo{
   220  				Owner:       ctx.Package.Owner,
   221  				PackageType: packages_model.TypeCargo,
   222  				Name:        cp.Name,
   223  				Version:     cp.Version,
   224  			},
   225  			SemverCompatible: true,
   226  			Creator:          ctx.Doer,
   227  			Metadata:         cp.Metadata,
   228  			VersionProperties: map[string]string{
   229  				cargo_module.PropertyYanked: strconv.FormatBool(false),
   230  			},
   231  		},
   232  		&packages_service.PackageFileCreationInfo{
   233  			PackageFileInfo: packages_service.PackageFileInfo{
   234  				Filename: strings.ToLower(fmt.Sprintf("%s-%s.crate", cp.Name, cp.Version)),
   235  			},
   236  			Creator: ctx.Doer,
   237  			Data:    buf,
   238  			IsLead:  true,
   239  		},
   240  	)
   241  	if err != nil {
   242  		switch err {
   243  		case packages_model.ErrDuplicatePackageVersion:
   244  			apiError(ctx, http.StatusConflict, err)
   245  		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   246  			apiError(ctx, http.StatusForbidden, err)
   247  		default:
   248  			apiError(ctx, http.StatusInternalServerError, err)
   249  		}
   250  		return
   251  	}
   252  
   253  	if err := cargo_service.UpdatePackageIndexIfExists(ctx, ctx.Doer, ctx.Package.Owner, pv.PackageID); err != nil {
   254  		if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
   255  			log.Error("Rollback creation of package version: %v", err)
   256  		}
   257  
   258  		apiError(ctx, http.StatusInternalServerError, err)
   259  		return
   260  	}
   261  
   262  	ctx.JSON(http.StatusOK, StatusResponse{OK: true})
   263  }
   264  
   265  // https://doc.rust-lang.org/cargo/reference/registries.html#yank
   266  func YankPackage(ctx *context.Context) {
   267  	yankPackage(ctx, true)
   268  }
   269  
   270  // https://doc.rust-lang.org/cargo/reference/registries.html#unyank
   271  func UnyankPackage(ctx *context.Context) {
   272  	yankPackage(ctx, false)
   273  }
   274  
   275  func yankPackage(ctx *context.Context, yank bool) {
   276  	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeCargo, ctx.Params("package"), ctx.Params("version"))
   277  	if err != nil {
   278  		if err == packages_model.ErrPackageNotExist {
   279  			apiError(ctx, http.StatusNotFound, err)
   280  			return
   281  		}
   282  		apiError(ctx, http.StatusInternalServerError, err)
   283  		return
   284  	}
   285  
   286  	pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, cargo_module.PropertyYanked)
   287  	if err != nil {
   288  		apiError(ctx, http.StatusInternalServerError, err)
   289  		return
   290  	}
   291  	if len(pps) == 0 {
   292  		apiError(ctx, http.StatusInternalServerError, "Property not found")
   293  		return
   294  	}
   295  
   296  	pp := pps[0]
   297  	pp.Value = strconv.FormatBool(yank)
   298  
   299  	if err := packages_model.UpdateProperty(ctx, pp); err != nil {
   300  		apiError(ctx, http.StatusInternalServerError, err)
   301  		return
   302  	}
   303  
   304  	if err := cargo_service.UpdatePackageIndexIfExists(ctx, ctx.Doer, ctx.Package.Owner, pv.PackageID); err != nil {
   305  		apiError(ctx, http.StatusInternalServerError, err)
   306  		return
   307  	}
   308  
   309  	ctx.JSON(http.StatusOK, StatusResponse{OK: true})
   310  }