mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-09-07 15:33:26 +00:00
Cargo registry-auth feature requires config.json to have a property auth-required set to true in order to send token to all registry requests. This is ok for git index because you can manually edit the config.json file to add the auth-required, but when using sparse (setting index url to "sparse+https://git.example.com/api/packages/{owner}/cargo/"), the config.json is dynamically rendered, and does not reflect changes to the config.json file in the repo. I see two approaches: - Serve the real config.json file when fetching the config.json on the cargo service. - Automatically detect if the registry requires authorization. (This is what I implemented in this PR). What the PR does: - When a cargo index repository is created, on the config.json, set auth-required to wether or not the repository is private. - When the cargo/config.json endpoint is called, set auth-required to wether or not the request was authorized using an API token.
309 lines
7.8 KiB
Go
309 lines
7.8 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"
|
|
repo_module "code.gitea.io/gitea/modules/repository"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/structs"
|
|
"code.gitea.io/gitea/modules/util"
|
|
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 := getOrCreateIndexRepository(ctx, doer, owner)
|
|
if err != nil {
|
|
return 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 AddOrUpdatePackageIndex(ctx context.Context, doer, owner *user_model.User, packageID int64) error {
|
|
repo, err := getOrCreateIndexRepository(ctx, doer, owner)
|
|
if err != nil {
|
|
return 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_module.CreateRepository(doer, owner, repo_module.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); err != nil {
|
|
if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
|
|
return err
|
|
}
|
|
if err := t.Init(); 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)
|
|
}
|