forgejo/services/packages/cargo/index.go
Gusted 461f925554
[BUG] Reflect Cargo index state in settings
- Currently in the Cargo section of the packages setting menu two
buttons are always shown, "Initalize index" and "Rebuild index", however
only of these should be shown depending on the state of the index, if
there's no index the "Initalize index" button should be shown and if
there's an index the "Rebuild index" button should be shown. This patch
does exactly that.
- Resolves #2628
2024-03-20 09:17:49 +01:00

315 lines
8.2 KiB
Go

// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cargo
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"path"
"strconv"
"time"
packages_model "code.gitea.io/gitea/models/packages"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
cargo_module "code.gitea.io/gitea/modules/packages/cargo"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
repo_service "code.gitea.io/gitea/services/repository"
files_service "code.gitea.io/gitea/services/repository/files"
)
const (
IndexRepositoryName = "_cargo-index"
ConfigFileName = "config.json"
)
// https://doc.rust-lang.org/cargo/reference/registries.html#index-format
func BuildPackagePath(name string) string {
switch len(name) {
case 0:
panic("Cargo package name can not be empty")
case 1:
return path.Join("1", name)
case 2:
return path.Join("2", name)
case 3:
return path.Join("3", string(name[0]), name)
default:
return path.Join(name[0:2], name[2:4], name)
}
}
func InitializeIndexRepository(ctx context.Context, doer, owner *user_model.User) error {
repo, err := getOrCreateIndexRepository(ctx, doer, owner)
if err != nil {
return err
}
if err := createOrUpdateConfigFile(ctx, repo, doer, owner); err != nil {
return fmt.Errorf("createOrUpdateConfigFile: %w", err)
}
return nil
}
func RebuildIndex(ctx context.Context, doer, owner *user_model.User) error {
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName)
if err != nil {
return fmt.Errorf("GetRepositoryByOwnerAndName: %w", err)
}
ps, err := packages_model.GetPackagesByType(ctx, owner.ID, packages_model.TypeCargo)
if err != nil {
return fmt.Errorf("GetPackagesByType: %w", err)
}
return alterRepositoryContent(
ctx,
doer,
repo,
"Rebuild Cargo Index",
func(t *files_service.TemporaryUploadRepository) error {
// Remove all existing content but the Cargo config
files, err := t.LsFiles()
if err != nil {
return err
}
for i, file := range files {
if file == ConfigFileName {
files[i] = files[len(files)-1]
files = files[:len(files)-1]
break
}
}
if err := t.RemoveFilesFromIndex(files...); err != nil {
return err
}
// Add all packages
for _, p := range ps {
if err := addOrUpdatePackageIndex(ctx, t, p); err != nil {
return err
}
}
return nil
},
)
}
func UpdatePackageIndexIfExists(ctx context.Context, doer, owner *user_model.User, packageID int64) error {
// We do not want to force the creation of the repo here
// cargo http index does not rely on the repo itself,
// so if the repo does not exist, we just do nothing.
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
return nil
}
return fmt.Errorf("GetRepositoryByOwnerAndName: %w", err)
}
p, err := packages_model.GetPackageByID(ctx, packageID)
if err != nil {
return fmt.Errorf("GetPackageByID[%d]: %w", packageID, err)
}
return alterRepositoryContent(
ctx,
doer,
repo,
"Update "+p.Name,
func(t *files_service.TemporaryUploadRepository) error {
return addOrUpdatePackageIndex(ctx, t, p)
},
)
}
type IndexVersionEntry struct {
Name string `json:"name"`
Version string `json:"vers"`
Dependencies []*cargo_module.Dependency `json:"deps"`
FileChecksum string `json:"cksum"`
Features map[string][]string `json:"features"`
Yanked bool `json:"yanked"`
Links string `json:"links,omitempty"`
}
func BuildPackageIndex(ctx context.Context, p *packages_model.Package) (*bytes.Buffer, error) {
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
PackageID: p.ID,
Sort: packages_model.SortVersionAsc,
})
if err != nil {
return nil, fmt.Errorf("SearchVersions[%s]: %w", p.Name, err)
}
if len(pvs) == 0 {
return nil, nil
}
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
if err != nil {
return nil, fmt.Errorf("GetPackageDescriptors[%s]: %w", p.Name, err)
}
var b bytes.Buffer
for _, pd := range pds {
metadata := pd.Metadata.(*cargo_module.Metadata)
dependencies := metadata.Dependencies
if dependencies == nil {
dependencies = make([]*cargo_module.Dependency, 0)
}
features := metadata.Features
if features == nil {
features = make(map[string][]string)
}
yanked, _ := strconv.ParseBool(pd.VersionProperties.GetByName(cargo_module.PropertyYanked))
entry, err := json.Marshal(&IndexVersionEntry{
Name: pd.Package.Name,
Version: pd.Version.Version,
Dependencies: dependencies,
FileChecksum: pd.Files[0].Blob.HashSHA256,
Features: features,
Yanked: yanked,
Links: metadata.Links,
})
if err != nil {
return nil, err
}
b.Write(entry)
b.WriteString("\n")
}
return &b, nil
}
func addOrUpdatePackageIndex(ctx context.Context, t *files_service.TemporaryUploadRepository, p *packages_model.Package) error {
b, err := BuildPackageIndex(ctx, p)
if err != nil {
return err
}
if b == nil {
return nil
}
return writeObjectToIndex(t, BuildPackagePath(p.LowerName), b)
}
func getOrCreateIndexRepository(ctx context.Context, doer, owner *user_model.User) (*repo_model.Repository, error) {
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
repo, err = repo_service.CreateRepositoryDirectly(ctx, doer, owner, repo_service.CreateRepoOptions{
Name: IndexRepositoryName,
})
if err != nil {
return nil, fmt.Errorf("CreateRepository: %w", err)
}
} else {
return nil, fmt.Errorf("GetRepositoryByOwnerAndName: %w", err)
}
}
return repo, nil
}
type Config struct {
DownloadURL string `json:"dl"`
APIURL string `json:"api"`
AuthRequired bool `json:"auth-required"`
}
func BuildConfig(owner *user_model.User, isPrivate bool) *Config {
return &Config{
DownloadURL: setting.AppURL + "api/packages/" + owner.Name + "/cargo/api/v1/crates",
APIURL: setting.AppURL + "api/packages/" + owner.Name + "/cargo",
AuthRequired: isPrivate,
}
}
func createOrUpdateConfigFile(ctx context.Context, repo *repo_model.Repository, doer, owner *user_model.User) error {
return alterRepositoryContent(
ctx,
doer,
repo,
"Initialize Cargo Config",
func(t *files_service.TemporaryUploadRepository) error {
var b bytes.Buffer
err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInView || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate))
if err != nil {
return err
}
return writeObjectToIndex(t, ConfigFileName, &b)
},
)
}
// This is a shorter version of CreateOrUpdateRepoFile which allows to perform multiple actions on a git repository
func alterRepositoryContent(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commitMessage string, fn func(*files_service.TemporaryUploadRepository) error) error {
t, err := files_service.NewTemporaryUploadRepository(ctx, repo)
if err != nil {
return err
}
defer t.Close()
var lastCommitID string
if err := t.Clone(repo.DefaultBranch, true); err != nil {
if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
return err
}
if err := t.Init(repo.ObjectFormatName); err != nil {
return err
}
} else {
if err := t.SetDefaultIndex(); err != nil {
return err
}
commit, err := t.GetBranchCommit(repo.DefaultBranch)
if err != nil {
return err
}
lastCommitID = commit.ID.String()
}
if err := fn(t); err != nil {
return err
}
treeHash, err := t.WriteTree()
if err != nil {
return err
}
now := time.Now()
commitHash, err := t.CommitTreeWithDate(lastCommitID, doer, doer, treeHash, commitMessage, false, now, now)
if err != nil {
return err
}
return t.Push(doer, commitHash, repo.DefaultBranch)
}
func writeObjectToIndex(t *files_service.TemporaryUploadRepository, path string, r io.Reader) error {
hash, err := t.HashObject(r)
if err != nil {
return err
}
return t.AddObjectToIndex("100644", hash, path)
}