code.gitea.io/gitea@v1.21.7/routers/api/packages/swift/swift.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package swift 5 6 import ( 7 "errors" 8 "fmt" 9 "io" 10 "net/http" 11 "regexp" 12 "sort" 13 "strings" 14 15 packages_model "code.gitea.io/gitea/models/packages" 16 "code.gitea.io/gitea/modules/context" 17 "code.gitea.io/gitea/modules/json" 18 "code.gitea.io/gitea/modules/log" 19 packages_module "code.gitea.io/gitea/modules/packages" 20 swift_module "code.gitea.io/gitea/modules/packages/swift" 21 "code.gitea.io/gitea/modules/setting" 22 "code.gitea.io/gitea/modules/util" 23 "code.gitea.io/gitea/routers/api/packages/helper" 24 packages_service "code.gitea.io/gitea/services/packages" 25 26 "github.com/hashicorp/go-version" 27 ) 28 29 // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning 30 const ( 31 AcceptJSON = "application/vnd.swift.registry.v1+json" 32 AcceptSwift = "application/vnd.swift.registry.v1+swift" 33 AcceptZip = "application/vnd.swift.registry.v1+zip" 34 ) 35 36 var ( 37 // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#361-package-scope 38 scopePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-]{0,38}\z`) 39 // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#362-package-name 40 namePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-_]{0,99}\z`) 41 ) 42 43 type headers struct { 44 Status int 45 ContentType string 46 Digest string 47 Location string 48 Link string 49 } 50 51 // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning 52 func setResponseHeaders(resp http.ResponseWriter, h *headers) { 53 if h.ContentType != "" { 54 resp.Header().Set("Content-Type", h.ContentType) 55 } 56 if h.Digest != "" { 57 resp.Header().Set("Digest", "sha256="+h.Digest) 58 } 59 if h.Location != "" { 60 resp.Header().Set("Location", h.Location) 61 } 62 if h.Link != "" { 63 resp.Header().Set("Link", h.Link) 64 } 65 resp.Header().Set("Content-Version", "1") 66 if h.Status != 0 { 67 resp.WriteHeader(h.Status) 68 } 69 } 70 71 // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#33-error-handling 72 func apiError(ctx *context.Context, status int, obj any) { 73 // https://www.rfc-editor.org/rfc/rfc7807 74 type Problem struct { 75 Status int `json:"status"` 76 Detail string `json:"detail"` 77 } 78 79 helper.LogAndProcessError(ctx, status, obj, func(message string) { 80 setResponseHeaders(ctx.Resp, &headers{ 81 Status: status, 82 ContentType: "application/problem+json", 83 }) 84 if err := json.NewEncoder(ctx.Resp).Encode(Problem{ 85 Status: status, 86 Detail: message, 87 }); err != nil { 88 log.Error("JSON encode: %v", err) 89 } 90 }) 91 } 92 93 // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning 94 func CheckAcceptMediaType(requiredAcceptHeader string) func(ctx *context.Context) { 95 return func(ctx *context.Context) { 96 accept := ctx.Req.Header.Get("Accept") 97 if accept != "" && accept != requiredAcceptHeader { 98 apiError(ctx, http.StatusBadRequest, fmt.Sprintf("Unexpected accept header. Should be '%s'.", requiredAcceptHeader)) 99 } 100 } 101 } 102 103 func buildPackageID(scope, name string) string { 104 return scope + "." + name 105 } 106 107 type Release struct { 108 URL string `json:"url"` 109 } 110 111 type EnumeratePackageVersionsResponse struct { 112 Releases map[string]Release `json:"releases"` 113 } 114 115 // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#41-list-package-releases 116 func EnumeratePackageVersions(ctx *context.Context) { 117 packageScope := ctx.Params("scope") 118 packageName := ctx.Params("name") 119 120 pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(packageScope, packageName)) 121 if err != nil { 122 apiError(ctx, http.StatusInternalServerError, err) 123 return 124 } 125 if len(pvs) == 0 { 126 apiError(ctx, http.StatusNotFound, nil) 127 return 128 } 129 130 pds, err := packages_model.GetPackageDescriptors(ctx, pvs) 131 if err != nil { 132 apiError(ctx, http.StatusInternalServerError, err) 133 return 134 } 135 136 sort.Slice(pds, func(i, j int) bool { 137 return pds[i].SemVer.LessThan(pds[j].SemVer) 138 }) 139 140 baseURL := fmt.Sprintf("%sapi/packages/%s/swift/%s/%s/", setting.AppURL, ctx.Package.Owner.LowerName, packageScope, packageName) 141 142 releases := make(map[string]Release) 143 for _, pd := range pds { 144 version := pd.SemVer.String() 145 releases[version] = Release{ 146 URL: baseURL + version, 147 } 148 } 149 150 setResponseHeaders(ctx.Resp, &headers{ 151 Link: fmt.Sprintf(`<%s%s>; rel="latest-version"`, baseURL, pds[len(pds)-1].Version.Version), 152 }) 153 154 ctx.JSON(http.StatusOK, EnumeratePackageVersionsResponse{ 155 Releases: releases, 156 }) 157 } 158 159 type Resource struct { 160 Name string `json:"name"` 161 Type string `json:"type"` 162 Checksum string `json:"checksum"` 163 } 164 165 type PackageVersionMetadataResponse struct { 166 ID string `json:"id"` 167 Version string `json:"version"` 168 Resources []Resource `json:"resources"` 169 Metadata *swift_module.SoftwareSourceCode `json:"metadata"` 170 } 171 172 // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-2 173 func PackageVersionMetadata(ctx *context.Context) { 174 id := buildPackageID(ctx.Params("scope"), ctx.Params("name")) 175 176 pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, id, ctx.Params("version")) 177 if err != nil { 178 if errors.Is(err, util.ErrNotExist) { 179 apiError(ctx, http.StatusNotFound, err) 180 } else { 181 apiError(ctx, http.StatusInternalServerError, err) 182 } 183 return 184 } 185 186 pd, err := packages_model.GetPackageDescriptor(ctx, pv) 187 if err != nil { 188 apiError(ctx, http.StatusInternalServerError, err) 189 return 190 } 191 192 metadata := pd.Metadata.(*swift_module.Metadata) 193 194 setResponseHeaders(ctx.Resp, &headers{}) 195 196 ctx.JSON(http.StatusOK, PackageVersionMetadataResponse{ 197 ID: id, 198 Version: pd.Version.Version, 199 Resources: []Resource{ 200 { 201 Name: "source-archive", 202 Type: "application/zip", 203 Checksum: pd.Files[0].Blob.HashSHA256, 204 }, 205 }, 206 Metadata: &swift_module.SoftwareSourceCode{ 207 Context: []string{"http://schema.org/"}, 208 Type: "SoftwareSourceCode", 209 Name: pd.PackageProperties.GetByName(swift_module.PropertyName), 210 Version: pd.Version.Version, 211 Description: metadata.Description, 212 Keywords: metadata.Keywords, 213 CodeRepository: metadata.RepositoryURL, 214 License: metadata.License, 215 ProgrammingLanguage: swift_module.ProgrammingLanguage{ 216 Type: "ComputerLanguage", 217 Name: "Swift", 218 URL: "https://swift.org", 219 }, 220 Author: swift_module.Person{ 221 Type: "Person", 222 GivenName: metadata.Author.GivenName, 223 MiddleName: metadata.Author.MiddleName, 224 FamilyName: metadata.Author.FamilyName, 225 }, 226 }, 227 }) 228 } 229 230 // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#43-fetch-manifest-for-a-package-release 231 func DownloadManifest(ctx *context.Context) { 232 packageScope := ctx.Params("scope") 233 packageName := ctx.Params("name") 234 packageVersion := ctx.Params("version") 235 236 pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(packageScope, packageName), packageVersion) 237 if err != nil { 238 if errors.Is(err, util.ErrNotExist) { 239 apiError(ctx, http.StatusNotFound, err) 240 } else { 241 apiError(ctx, http.StatusInternalServerError, err) 242 } 243 return 244 } 245 246 pd, err := packages_model.GetPackageDescriptor(ctx, pv) 247 if err != nil { 248 apiError(ctx, http.StatusInternalServerError, err) 249 return 250 } 251 252 swiftVersion := ctx.FormTrim("swift-version") 253 if swiftVersion != "" { 254 v, err := version.NewVersion(swiftVersion) 255 if err == nil { 256 swiftVersion = swift_module.TrimmedVersionString(v) 257 } 258 } 259 m, ok := pd.Metadata.(*swift_module.Metadata).Manifests[swiftVersion] 260 if !ok { 261 setResponseHeaders(ctx.Resp, &headers{ 262 Status: http.StatusSeeOther, 263 Location: fmt.Sprintf("%sapi/packages/%s/swift/%s/%s/%s/Package.swift", setting.AppURL, ctx.Package.Owner.LowerName, packageScope, packageName, packageVersion), 264 }) 265 return 266 } 267 268 setResponseHeaders(ctx.Resp, &headers{}) 269 270 filename := "Package.swift" 271 if swiftVersion != "" { 272 filename = fmt.Sprintf("Package@swift-%s.swift", swiftVersion) 273 } 274 275 ctx.ServeContent(strings.NewReader(m.Content), &context.ServeHeaderOptions{ 276 ContentType: "text/x-swift", 277 Filename: filename, 278 LastModified: pv.CreatedUnix.AsLocalTime(), 279 }) 280 } 281 282 // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-6 283 func UploadPackageFile(ctx *context.Context) { 284 packageScope := ctx.Params("scope") 285 packageName := ctx.Params("name") 286 287 v, err := version.NewVersion(ctx.Params("version")) 288 289 if !scopePattern.MatchString(packageScope) || !namePattern.MatchString(packageName) || err != nil { 290 apiError(ctx, http.StatusBadRequest, err) 291 return 292 } 293 294 packageVersion := v.Core().String() 295 296 file, _, err := ctx.Req.FormFile("source-archive") 297 if err != nil { 298 apiError(ctx, http.StatusBadRequest, err) 299 return 300 } 301 defer file.Close() 302 303 buf, err := packages_module.CreateHashedBufferFromReader(file) 304 if err != nil { 305 apiError(ctx, http.StatusInternalServerError, err) 306 return 307 } 308 defer buf.Close() 309 310 var mr io.Reader 311 metadata := ctx.Req.FormValue("metadata") 312 if metadata != "" { 313 mr = strings.NewReader(metadata) 314 } 315 316 pck, err := swift_module.ParsePackage(buf, buf.Size(), mr) 317 if err != nil { 318 if errors.Is(err, util.ErrInvalidArgument) { 319 apiError(ctx, http.StatusBadRequest, err) 320 } else { 321 apiError(ctx, http.StatusInternalServerError, err) 322 } 323 return 324 } 325 326 if _, err := buf.Seek(0, io.SeekStart); err != nil { 327 apiError(ctx, http.StatusInternalServerError, err) 328 return 329 } 330 331 pv, _, err := packages_service.CreatePackageAndAddFile( 332 ctx, 333 &packages_service.PackageCreationInfo{ 334 PackageInfo: packages_service.PackageInfo{ 335 Owner: ctx.Package.Owner, 336 PackageType: packages_model.TypeSwift, 337 Name: buildPackageID(packageScope, packageName), 338 Version: packageVersion, 339 }, 340 SemverCompatible: true, 341 Creator: ctx.Doer, 342 Metadata: pck.Metadata, 343 PackageProperties: map[string]string{ 344 swift_module.PropertyScope: packageScope, 345 swift_module.PropertyName: packageName, 346 }, 347 }, 348 &packages_service.PackageFileCreationInfo{ 349 PackageFileInfo: packages_service.PackageFileInfo{ 350 Filename: fmt.Sprintf("%s-%s.zip", packageName, packageVersion), 351 }, 352 Creator: ctx.Doer, 353 Data: buf, 354 IsLead: true, 355 }, 356 ) 357 if err != nil { 358 switch err { 359 case packages_model.ErrDuplicatePackageVersion: 360 apiError(ctx, http.StatusConflict, err) 361 case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: 362 apiError(ctx, http.StatusForbidden, err) 363 default: 364 apiError(ctx, http.StatusInternalServerError, err) 365 } 366 return 367 } 368 369 for _, url := range pck.RepositoryURLs { 370 _, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, swift_module.PropertyRepositoryURL, url) 371 if err != nil { 372 log.Error("InsertProperty failed: %v", err) 373 } 374 } 375 376 setResponseHeaders(ctx.Resp, &headers{}) 377 378 ctx.Status(http.StatusCreated) 379 } 380 381 // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-4 382 func DownloadPackageFile(ctx *context.Context) { 383 pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(ctx.Params("scope"), ctx.Params("name")), ctx.Params("version")) 384 if err != nil { 385 if errors.Is(err, util.ErrNotExist) { 386 apiError(ctx, http.StatusNotFound, err) 387 } else { 388 apiError(ctx, http.StatusInternalServerError, err) 389 } 390 return 391 } 392 393 pd, err := packages_model.GetPackageDescriptor(ctx, pv) 394 if err != nil { 395 apiError(ctx, http.StatusInternalServerError, err) 396 return 397 } 398 399 pf := pd.Files[0].File 400 401 s, u, _, err := packages_service.GetPackageFileStream(ctx, pf) 402 if err != nil { 403 apiError(ctx, http.StatusInternalServerError, err) 404 return 405 } 406 407 setResponseHeaders(ctx.Resp, &headers{ 408 Digest: pd.Files[0].Blob.HashSHA256, 409 }) 410 411 helper.ServePackageFile(ctx, s, u, pf, &context.ServeHeaderOptions{ 412 Filename: pf.Name, 413 ContentType: "application/zip", 414 LastModified: pf.CreatedUnix.AsLocalTime(), 415 }) 416 } 417 418 type LookupPackageIdentifiersResponse struct { 419 Identifiers []string `json:"identifiers"` 420 } 421 422 // https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-5 423 func LookupPackageIdentifiers(ctx *context.Context) { 424 url := ctx.FormTrim("url") 425 if url == "" { 426 apiError(ctx, http.StatusBadRequest, nil) 427 return 428 } 429 430 pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{ 431 OwnerID: ctx.Package.Owner.ID, 432 Type: packages_model.TypeSwift, 433 Properties: map[string]string{ 434 swift_module.PropertyRepositoryURL: url, 435 }, 436 IsInternal: util.OptionalBoolFalse, 437 }) 438 if err != nil { 439 apiError(ctx, http.StatusInternalServerError, err) 440 return 441 } 442 443 if len(pvs) == 0 { 444 apiError(ctx, http.StatusNotFound, nil) 445 return 446 } 447 448 pds, err := packages_model.GetPackageDescriptors(ctx, pvs) 449 if err != nil { 450 apiError(ctx, http.StatusInternalServerError, err) 451 return 452 } 453 454 identifiers := make([]string, 0, len(pds)) 455 for _, pd := range pds { 456 identifiers = append(identifiers, pd.Package.Name) 457 } 458 459 setResponseHeaders(ctx.Resp, &headers{}) 460 461 ctx.JSON(http.StatusOK, LookupPackageIdentifiersResponse{ 462 Identifiers: identifiers, 463 }) 464 }