gitea
Зеркало из https://github.com/go-gitea/gitea
1// Copyright 2022 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package cargo
5
6import (
7"bytes"
8"context"
9"errors"
10"fmt"
11"io"
12"path"
13"strconv"
14"time"
15
16packages_model "code.gitea.io/gitea/models/packages"
17repo_model "code.gitea.io/gitea/models/repo"
18user_model "code.gitea.io/gitea/models/user"
19"code.gitea.io/gitea/modules/git"
20"code.gitea.io/gitea/modules/json"
21cargo_module "code.gitea.io/gitea/modules/packages/cargo"
22"code.gitea.io/gitea/modules/setting"
23"code.gitea.io/gitea/modules/structs"
24"code.gitea.io/gitea/modules/util"
25repo_service "code.gitea.io/gitea/services/repository"
26files_service "code.gitea.io/gitea/services/repository/files"
27)
28
29const (
30IndexRepositoryName = "_cargo-index"
31ConfigFileName = "config.json"
32)
33
34// https://doc.rust-lang.org/cargo/reference/registries.html#index-format
35
36func BuildPackagePath(name string) string {
37switch len(name) {
38case 0:
39panic("Cargo package name can not be empty")
40case 1:
41return path.Join("1", name)
42case 2:
43return path.Join("2", name)
44case 3:
45return path.Join("3", string(name[0]), name)
46default:
47return path.Join(name[0:2], name[2:4], name)
48}
49}
50
51func InitializeIndexRepository(ctx context.Context, doer, owner *user_model.User) error {
52repo, err := getOrCreateIndexRepository(ctx, doer, owner)
53if err != nil {
54return err
55}
56
57if err := createOrUpdateConfigFile(ctx, repo, doer, owner); err != nil {
58return fmt.Errorf("createOrUpdateConfigFile: %w", err)
59}
60
61return nil
62}
63
64func RebuildIndex(ctx context.Context, doer, owner *user_model.User) error {
65repo, err := getOrCreateIndexRepository(ctx, doer, owner)
66if err != nil {
67return err
68}
69
70ps, err := packages_model.GetPackagesByType(ctx, owner.ID, packages_model.TypeCargo)
71if err != nil {
72return fmt.Errorf("GetPackagesByType: %w", err)
73}
74
75return alterRepositoryContent(
76ctx,
77doer,
78repo,
79"Rebuild Cargo Index",
80func(t *files_service.TemporaryUploadRepository) error {
81// Remove all existing content but the Cargo config
82files, err := t.LsFiles()
83if err != nil {
84return err
85}
86for i, file := range files {
87if file == ConfigFileName {
88files[i] = files[len(files)-1]
89files = files[:len(files)-1]
90break
91}
92}
93if err := t.RemoveFilesFromIndex(files...); err != nil {
94return err
95}
96
97// Add all packages
98for _, p := range ps {
99if err := addOrUpdatePackageIndex(ctx, t, p); err != nil {
100return err
101}
102}
103
104return nil
105},
106)
107}
108
109func UpdatePackageIndexIfExists(ctx context.Context, doer, owner *user_model.User, packageID int64) error {
110// We do not want to force the creation of the repo here
111// cargo http index does not rely on the repo itself,
112// so if the repo does not exist, we just do nothing.
113repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName)
114if err != nil {
115if errors.Is(err, util.ErrNotExist) {
116return nil
117}
118return fmt.Errorf("GetRepositoryByOwnerAndName: %w", err)
119}
120
121p, err := packages_model.GetPackageByID(ctx, packageID)
122if err != nil {
123return fmt.Errorf("GetPackageByID[%d]: %w", packageID, err)
124}
125
126return alterRepositoryContent(
127ctx,
128doer,
129repo,
130"Update "+p.Name,
131func(t *files_service.TemporaryUploadRepository) error {
132return addOrUpdatePackageIndex(ctx, t, p)
133},
134)
135}
136
137type IndexVersionEntry struct {
138Name string `json:"name"`
139Version string `json:"vers"`
140Dependencies []*cargo_module.Dependency `json:"deps"`
141FileChecksum string `json:"cksum"`
142Features map[string][]string `json:"features"`
143Yanked bool `json:"yanked"`
144Links string `json:"links,omitempty"`
145}
146
147func BuildPackageIndex(ctx context.Context, p *packages_model.Package) (*bytes.Buffer, error) {
148pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
149PackageID: p.ID,
150Sort: packages_model.SortVersionAsc,
151})
152if err != nil {
153return nil, fmt.Errorf("SearchVersions[%s]: %w", p.Name, err)
154}
155if len(pvs) == 0 {
156return nil, nil
157}
158
159pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
160if err != nil {
161return nil, fmt.Errorf("GetPackageDescriptors[%s]: %w", p.Name, err)
162}
163
164var b bytes.Buffer
165for _, pd := range pds {
166metadata := pd.Metadata.(*cargo_module.Metadata)
167
168dependencies := metadata.Dependencies
169if dependencies == nil {
170dependencies = make([]*cargo_module.Dependency, 0)
171}
172
173features := metadata.Features
174if features == nil {
175features = make(map[string][]string)
176}
177
178yanked, _ := strconv.ParseBool(pd.VersionProperties.GetByName(cargo_module.PropertyYanked))
179entry, err := json.Marshal(&IndexVersionEntry{
180Name: pd.Package.Name,
181Version: pd.Version.Version,
182Dependencies: dependencies,
183FileChecksum: pd.Files[0].Blob.HashSHA256,
184Features: features,
185Yanked: yanked,
186Links: metadata.Links,
187})
188if err != nil {
189return nil, err
190}
191
192b.Write(entry)
193b.WriteString("\n")
194}
195
196return &b, nil
197}
198
199func addOrUpdatePackageIndex(ctx context.Context, t *files_service.TemporaryUploadRepository, p *packages_model.Package) error {
200b, err := BuildPackageIndex(ctx, p)
201if err != nil {
202return err
203}
204if b == nil {
205return nil
206}
207
208return writeObjectToIndex(t, BuildPackagePath(p.LowerName), b)
209}
210
211func getOrCreateIndexRepository(ctx context.Context, doer, owner *user_model.User) (*repo_model.Repository, error) {
212repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName)
213if err != nil {
214if errors.Is(err, util.ErrNotExist) {
215repo, err = repo_service.CreateRepositoryDirectly(ctx, doer, owner, repo_service.CreateRepoOptions{
216Name: IndexRepositoryName,
217})
218if err != nil {
219return nil, fmt.Errorf("CreateRepository: %w", err)
220}
221} else {
222return nil, fmt.Errorf("GetRepositoryByOwnerAndName: %w", err)
223}
224}
225
226return repo, nil
227}
228
229type Config struct {
230DownloadURL string `json:"dl"`
231APIURL string `json:"api"`
232AuthRequired bool `json:"auth-required"`
233}
234
235func BuildConfig(owner *user_model.User, isPrivate bool) *Config {
236return &Config{
237DownloadURL: setting.AppURL + "api/packages/" + owner.Name + "/cargo/api/v1/crates",
238APIURL: setting.AppURL + "api/packages/" + owner.Name + "/cargo",
239AuthRequired: isPrivate,
240}
241}
242
243func createOrUpdateConfigFile(ctx context.Context, repo *repo_model.Repository, doer, owner *user_model.User) error {
244return alterRepositoryContent(
245ctx,
246doer,
247repo,
248"Initialize Cargo Config",
249func(t *files_service.TemporaryUploadRepository) error {
250var b bytes.Buffer
251err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInView || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate))
252if err != nil {
253return err
254}
255
256return writeObjectToIndex(t, ConfigFileName, &b)
257},
258)
259}
260
261// This is a shorter version of CreateOrUpdateRepoFile which allows to perform multiple actions on a git repository
262func alterRepositoryContent(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commitMessage string, fn func(*files_service.TemporaryUploadRepository) error) error {
263t, err := files_service.NewTemporaryUploadRepository(ctx, repo)
264if err != nil {
265return err
266}
267defer t.Close()
268
269var lastCommitID string
270if err := t.Clone(repo.DefaultBranch, true); err != nil {
271if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
272return err
273}
274if err := t.Init(repo.ObjectFormatName); err != nil {
275return err
276}
277} else {
278if err := t.SetDefaultIndex(); err != nil {
279return err
280}
281
282commit, err := t.GetBranchCommit(repo.DefaultBranch)
283if err != nil {
284return err
285}
286
287lastCommitID = commit.ID.String()
288}
289
290if err := fn(t); err != nil {
291return err
292}
293
294treeHash, err := t.WriteTree()
295if err != nil {
296return err
297}
298
299now := time.Now()
300commitHash, err := t.CommitTreeWithDate(lastCommitID, doer, doer, treeHash, commitMessage, false, now, now)
301if err != nil {
302return err
303}
304
305return t.Push(doer, commitHash, repo.DefaultBranch)
306}
307
308func writeObjectToIndex(t *files_service.TemporaryUploadRepository, path string, r io.Reader) error {
309hash, err := t.HashObject(r)
310if err != nil {
311return err
312}
313
314return t.AddObjectToIndex("100644", hash, path)
315}
316