mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 08:34:30 +01:00 
			
		
		
		
	This implements the HTTP index [RFC](https://rust-lang.github.io/rfcs/2789-sparse-index.html) for Cargo registries. Currently this is a preview feature and you need to use the nightly of `cargo`: `cargo +nightly -Z sparse-registry update` See https://github.com/rust-lang/cargo/issues/9069 for more information. --------- Co-authored-by: Giteabot <teabot@gitea.io>
		
			
				
	
	
		
			307 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			307 lines
		
	
	
		
			7.6 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/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"`
 | 
						|
}
 | 
						|
 | 
						|
func BuildConfig(owner *user_model.User) *Config {
 | 
						|
	return &Config{
 | 
						|
		DownloadURL: setting.AppURL + "api/packages/" + owner.Name + "/cargo/api/v1/crates",
 | 
						|
		APIURL:      setting.AppURL + "api/packages/" + owner.Name + "/cargo",
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
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))
 | 
						|
			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)
 | 
						|
}
 |