// 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 addOrUpdatePackageIndex(ctx context.Context, t *files_service.TemporaryUploadRepository, p *packages_model.Package) error { pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ PackageID: p.ID, Sort: packages_model.SortVersionAsc, }) if err != nil { return fmt.Errorf("SearchVersions[%s]: %w", p.Name, err) } if len(pvs) == 0 { return nil } pds, err := packages_model.GetPackageDescriptors(ctx, pvs) if err != nil { return 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 err } b.Write(entry) b.WriteString("\n") } return writeObjectToIndex(t, BuildPackagePath(pds[0].Package.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 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(Config{ DownloadURL: setting.AppURL + "api/packages/" + owner.Name + "/cargo/api/v1/crates", APIURL: setting.AppURL + "api/packages/" + owner.Name + "/cargo", }) 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) }