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