310 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			310 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)
 | |
| }
 |