code.gitea.io/gitea@v1.21.7/routers/api/packages/pub/pub.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package pub
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"sort"
    13  	"strings"
    14  	"time"
    15  
    16  	packages_model "code.gitea.io/gitea/models/packages"
    17  	"code.gitea.io/gitea/modules/context"
    18  	"code.gitea.io/gitea/modules/json"
    19  	"code.gitea.io/gitea/modules/log"
    20  	packages_module "code.gitea.io/gitea/modules/packages"
    21  	pub_module "code.gitea.io/gitea/modules/packages/pub"
    22  	"code.gitea.io/gitea/modules/setting"
    23  	"code.gitea.io/gitea/modules/util"
    24  	"code.gitea.io/gitea/routers/api/packages/helper"
    25  	packages_service "code.gitea.io/gitea/services/packages"
    26  )
    27  
    28  func jsonResponse(ctx *context.Context, status int, obj any) {
    29  	resp := ctx.Resp
    30  	resp.Header().Set("Content-Type", "application/vnd.pub.v2+json")
    31  	resp.WriteHeader(status)
    32  	if err := json.NewEncoder(resp).Encode(obj); err != nil {
    33  		log.Error("JSON encode: %v", err)
    34  	}
    35  }
    36  
    37  func apiError(ctx *context.Context, status int, obj any) {
    38  	type Error struct {
    39  		Code    string `json:"code"`
    40  		Message string `json:"message"`
    41  	}
    42  	type ErrorWrapper struct {
    43  		Error Error `json:"error"`
    44  	}
    45  
    46  	helper.LogAndProcessError(ctx, status, obj, func(message string) {
    47  		jsonResponse(ctx, status, ErrorWrapper{
    48  			Error: Error{
    49  				Code:    http.StatusText(status),
    50  				Message: message,
    51  			},
    52  		})
    53  	})
    54  }
    55  
    56  type packageVersions struct {
    57  	Name     string             `json:"name"`
    58  	Latest   *versionMetadata   `json:"latest"`
    59  	Versions []*versionMetadata `json:"versions"`
    60  }
    61  
    62  type versionMetadata struct {
    63  	Version    string    `json:"version"`
    64  	ArchiveURL string    `json:"archive_url"`
    65  	Published  time.Time `json:"published"`
    66  	Pubspec    any       `json:"pubspec,omitempty"`
    67  }
    68  
    69  func packageDescriptorToMetadata(baseURL string, pd *packages_model.PackageDescriptor) *versionMetadata {
    70  	return &versionMetadata{
    71  		Version:    pd.Version.Version,
    72  		ArchiveURL: fmt.Sprintf("%s/files/%s.tar.gz", baseURL, url.PathEscape(pd.Version.Version)),
    73  		Published:  pd.Version.CreatedUnix.AsLocalTime(),
    74  		Pubspec:    pd.Metadata.(*pub_module.Metadata).Pubspec,
    75  	}
    76  }
    77  
    78  func baseURL(ctx *context.Context) string {
    79  	return setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/pub/api/packages"
    80  }
    81  
    82  // https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#list-all-versions-of-a-package
    83  func EnumeratePackageVersions(ctx *context.Context) {
    84  	packageName := ctx.Params("id")
    85  
    86  	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName)
    87  	if err != nil {
    88  		apiError(ctx, http.StatusInternalServerError, err)
    89  		return
    90  	}
    91  	if len(pvs) == 0 {
    92  		apiError(ctx, http.StatusNotFound, err)
    93  		return
    94  	}
    95  
    96  	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
    97  	if err != nil {
    98  		apiError(ctx, http.StatusInternalServerError, err)
    99  		return
   100  	}
   101  
   102  	sort.Slice(pds, func(i, j int) bool {
   103  		return pds[i].SemVer.LessThan(pds[j].SemVer)
   104  	})
   105  
   106  	baseURL := fmt.Sprintf("%s/%s", baseURL(ctx), url.PathEscape(pds[0].Package.Name))
   107  
   108  	versions := make([]*versionMetadata, 0, len(pds))
   109  	for _, pd := range pds {
   110  		versions = append(versions, packageDescriptorToMetadata(baseURL, pd))
   111  	}
   112  
   113  	jsonResponse(ctx, http.StatusOK, &packageVersions{
   114  		Name:     pds[0].Package.Name,
   115  		Latest:   packageDescriptorToMetadata(baseURL, pds[0]),
   116  		Versions: versions,
   117  	})
   118  }
   119  
   120  // https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#deprecated-inspect-a-specific-version-of-a-package
   121  func PackageVersionMetadata(ctx *context.Context) {
   122  	packageName := ctx.Params("id")
   123  	packageVersion := ctx.Params("version")
   124  
   125  	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName, packageVersion)
   126  	if err != nil {
   127  		if err == packages_model.ErrPackageNotExist {
   128  			apiError(ctx, http.StatusNotFound, err)
   129  			return
   130  		}
   131  		apiError(ctx, http.StatusInternalServerError, err)
   132  		return
   133  	}
   134  
   135  	pd, err := packages_model.GetPackageDescriptor(ctx, pv)
   136  	if err != nil {
   137  		apiError(ctx, http.StatusInternalServerError, err)
   138  		return
   139  	}
   140  
   141  	jsonResponse(ctx, http.StatusOK, packageDescriptorToMetadata(
   142  		fmt.Sprintf("%s/%s", baseURL(ctx), url.PathEscape(pd.Package.Name)),
   143  		pd,
   144  	))
   145  }
   146  
   147  // https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#publishing-packages
   148  func RequestUpload(ctx *context.Context) {
   149  	type UploadRequest struct {
   150  		URL    string            `json:"url"`
   151  		Fields map[string]string `json:"fields"`
   152  	}
   153  
   154  	jsonResponse(ctx, http.StatusOK, UploadRequest{
   155  		URL:    baseURL(ctx) + "/versions/new/upload",
   156  		Fields: make(map[string]string),
   157  	})
   158  }
   159  
   160  // https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#publishing-packages
   161  func UploadPackageFile(ctx *context.Context) {
   162  	file, _, err := ctx.Req.FormFile("file")
   163  	if err != nil {
   164  		apiError(ctx, http.StatusBadRequest, err)
   165  		return
   166  	}
   167  	defer file.Close()
   168  
   169  	buf, err := packages_module.CreateHashedBufferFromReader(file)
   170  	if err != nil {
   171  		apiError(ctx, http.StatusInternalServerError, err)
   172  		return
   173  	}
   174  	defer buf.Close()
   175  
   176  	pck, err := pub_module.ParsePackage(buf)
   177  	if err != nil {
   178  		if errors.Is(err, util.ErrInvalidArgument) {
   179  			apiError(ctx, http.StatusBadRequest, err)
   180  		} else {
   181  			apiError(ctx, http.StatusInternalServerError, err)
   182  		}
   183  		return
   184  	}
   185  
   186  	if _, err := buf.Seek(0, io.SeekStart); err != nil {
   187  		apiError(ctx, http.StatusInternalServerError, err)
   188  		return
   189  	}
   190  
   191  	_, _, err = packages_service.CreatePackageAndAddFile(
   192  		ctx,
   193  		&packages_service.PackageCreationInfo{
   194  			PackageInfo: packages_service.PackageInfo{
   195  				Owner:       ctx.Package.Owner,
   196  				PackageType: packages_model.TypePub,
   197  				Name:        pck.Name,
   198  				Version:     pck.Version,
   199  			},
   200  			SemverCompatible: true,
   201  			Creator:          ctx.Doer,
   202  			Metadata:         pck.Metadata,
   203  		},
   204  		&packages_service.PackageFileCreationInfo{
   205  			PackageFileInfo: packages_service.PackageFileInfo{
   206  				Filename: strings.ToLower(pck.Version + ".tar.gz"),
   207  			},
   208  			Creator: ctx.Doer,
   209  			Data:    buf,
   210  			IsLead:  true,
   211  		},
   212  	)
   213  	if err != nil {
   214  		switch err {
   215  		case packages_model.ErrDuplicatePackageVersion:
   216  			apiError(ctx, http.StatusBadRequest, err)
   217  		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   218  			apiError(ctx, http.StatusForbidden, err)
   219  		default:
   220  			apiError(ctx, http.StatusInternalServerError, err)
   221  		}
   222  		return
   223  	}
   224  
   225  	ctx.Resp.Header().Set("Location", fmt.Sprintf("%s/versions/new/finalize/%s/%s", baseURL(ctx), url.PathEscape(pck.Name), url.PathEscape(pck.Version)))
   226  	ctx.Status(http.StatusNoContent)
   227  }
   228  
   229  // https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#publishing-packages
   230  func FinalizePackage(ctx *context.Context) {
   231  	packageName := ctx.Params("id")
   232  	packageVersion := ctx.Params("version")
   233  
   234  	_, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName, packageVersion)
   235  	if err != nil {
   236  		if err == packages_model.ErrPackageNotExist {
   237  			apiError(ctx, http.StatusNotFound, err)
   238  			return
   239  		}
   240  		apiError(ctx, http.StatusInternalServerError, err)
   241  		return
   242  	}
   243  
   244  	type Success struct {
   245  		Message string `json:"message"`
   246  	}
   247  	type SuccessWrapper struct {
   248  		Success Success `json:"success"`
   249  	}
   250  
   251  	jsonResponse(ctx, http.StatusOK, SuccessWrapper{Success{}})
   252  }
   253  
   254  // https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#deprecated-download-a-specific-version-of-a-package
   255  func DownloadPackageFile(ctx *context.Context) {
   256  	packageName := ctx.Params("id")
   257  	packageVersion := strings.TrimSuffix(ctx.Params("version"), ".tar.gz")
   258  
   259  	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName, packageVersion)
   260  	if err != nil {
   261  		if err == packages_model.ErrPackageNotExist {
   262  			apiError(ctx, http.StatusNotFound, err)
   263  			return
   264  		}
   265  		apiError(ctx, http.StatusInternalServerError, err)
   266  		return
   267  	}
   268  
   269  	pd, err := packages_model.GetPackageDescriptor(ctx, pv)
   270  	if err != nil {
   271  		apiError(ctx, http.StatusInternalServerError, err)
   272  		return
   273  	}
   274  
   275  	pf := pd.Files[0].File
   276  
   277  	s, u, _, err := packages_service.GetPackageFileStream(ctx, pf)
   278  	if err != nil {
   279  		apiError(ctx, http.StatusInternalServerError, err)
   280  		return
   281  	}
   282  
   283  	helper.ServePackageFile(ctx, s, u, pf)
   284  }