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