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

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package conda
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"strings"
    12  
    13  	packages_model "code.gitea.io/gitea/models/packages"
    14  	conda_model "code.gitea.io/gitea/models/packages/conda"
    15  	"code.gitea.io/gitea/modules/context"
    16  	"code.gitea.io/gitea/modules/json"
    17  	"code.gitea.io/gitea/modules/log"
    18  	packages_module "code.gitea.io/gitea/modules/packages"
    19  	conda_module "code.gitea.io/gitea/modules/packages/conda"
    20  	"code.gitea.io/gitea/modules/util"
    21  	"code.gitea.io/gitea/routers/api/packages/helper"
    22  	packages_service "code.gitea.io/gitea/services/packages"
    23  
    24  	"github.com/dsnet/compress/bzip2"
    25  )
    26  
    27  func apiError(ctx *context.Context, status int, obj any) {
    28  	helper.LogAndProcessError(ctx, status, obj, func(message string) {
    29  		ctx.JSON(status, struct {
    30  			Reason  string `json:"reason"`
    31  			Message string `json:"message"`
    32  		}{
    33  			Reason:  http.StatusText(status),
    34  			Message: message,
    35  		})
    36  	})
    37  }
    38  
    39  func EnumeratePackages(ctx *context.Context) {
    40  	type Info struct {
    41  		Subdir string `json:"subdir"`
    42  	}
    43  
    44  	type PackageInfo struct {
    45  		Name          string   `json:"name"`
    46  		Version       string   `json:"version"`
    47  		NoArch        string   `json:"noarch"`
    48  		Subdir        string   `json:"subdir"`
    49  		Timestamp     int64    `json:"timestamp"`
    50  		Build         string   `json:"build"`
    51  		BuildNumber   int64    `json:"build_number"`
    52  		Dependencies  []string `json:"depends"`
    53  		License       string   `json:"license"`
    54  		LicenseFamily string   `json:"license_family"`
    55  		HashMD5       string   `json:"md5"`
    56  		HashSHA256    string   `json:"sha256"`
    57  		Size          int64    `json:"size"`
    58  	}
    59  
    60  	type RepoData struct {
    61  		Info          Info                    `json:"info"`
    62  		Packages      map[string]*PackageInfo `json:"packages"`
    63  		PackagesConda map[string]*PackageInfo `json:"packages.conda"`
    64  		Removed       map[string]*PackageInfo `json:"removed"`
    65  	}
    66  
    67  	repoData := &RepoData{
    68  		Info: Info{
    69  			Subdir: ctx.Params("architecture"),
    70  		},
    71  		Packages:      make(map[string]*PackageInfo),
    72  		PackagesConda: make(map[string]*PackageInfo),
    73  		Removed:       make(map[string]*PackageInfo),
    74  	}
    75  
    76  	pfs, err := conda_model.SearchFiles(ctx, &conda_model.FileSearchOptions{
    77  		OwnerID: ctx.Package.Owner.ID,
    78  		Channel: ctx.Params("channel"),
    79  		Subdir:  repoData.Info.Subdir,
    80  	})
    81  	if err != nil {
    82  		apiError(ctx, http.StatusInternalServerError, err)
    83  		return
    84  	}
    85  
    86  	if len(pfs) == 0 {
    87  		apiError(ctx, http.StatusNotFound, nil)
    88  		return
    89  	}
    90  
    91  	pds := make(map[int64]*packages_model.PackageDescriptor)
    92  
    93  	for _, pf := range pfs {
    94  		pd, exists := pds[pf.VersionID]
    95  		if !exists {
    96  			pv, err := packages_model.GetVersionByID(ctx, pf.VersionID)
    97  			if err != nil {
    98  				apiError(ctx, http.StatusInternalServerError, err)
    99  				return
   100  			}
   101  
   102  			pd, err = packages_model.GetPackageDescriptor(ctx, pv)
   103  			if err != nil {
   104  				apiError(ctx, http.StatusInternalServerError, err)
   105  				return
   106  			}
   107  
   108  			pds[pf.VersionID] = pd
   109  		}
   110  
   111  		var pfd *packages_model.PackageFileDescriptor
   112  		for _, d := range pd.Files {
   113  			if d.File.ID == pf.ID {
   114  				pfd = d
   115  				break
   116  			}
   117  		}
   118  
   119  		var fileMetadata *conda_module.FileMetadata
   120  		if err := json.Unmarshal([]byte(pfd.Properties.GetByName(conda_module.PropertyMetadata)), &fileMetadata); err != nil {
   121  			apiError(ctx, http.StatusInternalServerError, err)
   122  			return
   123  		}
   124  
   125  		versionMetadata := pd.Metadata.(*conda_module.VersionMetadata)
   126  
   127  		pi := &PackageInfo{
   128  			Name:          pd.PackageProperties.GetByName(conda_module.PropertyName),
   129  			Version:       pd.Version.Version,
   130  			NoArch:        fileMetadata.NoArch,
   131  			Subdir:        repoData.Info.Subdir,
   132  			Timestamp:     fileMetadata.Timestamp,
   133  			Build:         fileMetadata.Build,
   134  			BuildNumber:   fileMetadata.BuildNumber,
   135  			Dependencies:  fileMetadata.Dependencies,
   136  			License:       versionMetadata.License,
   137  			LicenseFamily: versionMetadata.LicenseFamily,
   138  			HashMD5:       pfd.Blob.HashMD5,
   139  			HashSHA256:    pfd.Blob.HashSHA256,
   140  			Size:          pfd.Blob.Size,
   141  		}
   142  
   143  		if fileMetadata.IsCondaPackage {
   144  			repoData.PackagesConda[pfd.File.Name] = pi
   145  		} else {
   146  			repoData.Packages[pfd.File.Name] = pi
   147  		}
   148  	}
   149  
   150  	resp := ctx.Resp
   151  
   152  	var w io.Writer = resp
   153  
   154  	if strings.HasSuffix(ctx.Params("filename"), ".json") {
   155  		resp.Header().Set("Content-Type", "application/json")
   156  	} else {
   157  		resp.Header().Set("Content-Type", "application/x-bzip2")
   158  
   159  		zw, err := bzip2.NewWriter(w, nil)
   160  		if err != nil {
   161  			apiError(ctx, http.StatusInternalServerError, err)
   162  			return
   163  		}
   164  		defer zw.Close()
   165  
   166  		w = zw
   167  	}
   168  
   169  	resp.WriteHeader(http.StatusOK)
   170  
   171  	if err := json.NewEncoder(w).Encode(repoData); err != nil {
   172  		log.Error("JSON encode: %v", err)
   173  	}
   174  }
   175  
   176  func UploadPackageFile(ctx *context.Context) {
   177  	upload, close, err := ctx.UploadStream()
   178  	if err != nil {
   179  		apiError(ctx, http.StatusInternalServerError, err)
   180  		return
   181  	}
   182  	if close {
   183  		defer upload.Close()
   184  	}
   185  
   186  	buf, err := packages_module.CreateHashedBufferFromReader(upload)
   187  	if err != nil {
   188  		apiError(ctx, http.StatusInternalServerError, err)
   189  		return
   190  	}
   191  	defer buf.Close()
   192  
   193  	var pck *conda_module.Package
   194  	if strings.HasSuffix(strings.ToLower(ctx.Params("filename")), ".tar.bz2") {
   195  		pck, err = conda_module.ParsePackageBZ2(buf)
   196  	} else {
   197  		pck, err = conda_module.ParsePackageConda(buf, buf.Size())
   198  	}
   199  	if err != nil {
   200  		if errors.Is(err, util.ErrInvalidArgument) {
   201  			apiError(ctx, http.StatusBadRequest, err)
   202  		} else {
   203  			apiError(ctx, http.StatusInternalServerError, err)
   204  		}
   205  		return
   206  	}
   207  
   208  	if _, err := buf.Seek(0, io.SeekStart); err != nil {
   209  		apiError(ctx, http.StatusInternalServerError, err)
   210  		return
   211  	}
   212  
   213  	fullName := pck.Name
   214  
   215  	channel := ctx.Params("channel")
   216  	if channel != "" {
   217  		fullName = channel + "/" + pck.Name
   218  	}
   219  
   220  	extension := ".tar.bz2"
   221  	if pck.FileMetadata.IsCondaPackage {
   222  		extension = ".conda"
   223  	}
   224  
   225  	fileMetadataRaw, err := json.Marshal(pck.FileMetadata)
   226  	if err != nil {
   227  		apiError(ctx, http.StatusInternalServerError, err)
   228  		return
   229  	}
   230  
   231  	_, _, err = packages_service.CreatePackageOrAddFileToExisting(
   232  		ctx,
   233  		&packages_service.PackageCreationInfo{
   234  			PackageInfo: packages_service.PackageInfo{
   235  				Owner:       ctx.Package.Owner,
   236  				PackageType: packages_model.TypeConda,
   237  				Name:        fullName,
   238  				Version:     pck.Version,
   239  			},
   240  			SemverCompatible: false,
   241  			Creator:          ctx.Doer,
   242  			Metadata:         pck.VersionMetadata,
   243  			PackageProperties: map[string]string{
   244  				conda_module.PropertyName:    pck.Name,
   245  				conda_module.PropertyChannel: channel,
   246  			},
   247  		},
   248  		&packages_service.PackageFileCreationInfo{
   249  			PackageFileInfo: packages_service.PackageFileInfo{
   250  				Filename:     fmt.Sprintf("%s-%s-%s%s", pck.Name, pck.Version, pck.FileMetadata.Build, extension),
   251  				CompositeKey: pck.Subdir,
   252  			},
   253  			Creator: ctx.Doer,
   254  			Data:    buf,
   255  			IsLead:  true,
   256  			Properties: map[string]string{
   257  				conda_module.PropertySubdir:   pck.Subdir,
   258  				conda_module.PropertyMetadata: string(fileMetadataRaw),
   259  			},
   260  		},
   261  	)
   262  	if err != nil {
   263  		switch err {
   264  		case packages_model.ErrDuplicatePackageFile:
   265  			apiError(ctx, http.StatusConflict, err)
   266  		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   267  			apiError(ctx, http.StatusForbidden, err)
   268  		default:
   269  			apiError(ctx, http.StatusInternalServerError, err)
   270  		}
   271  		return
   272  	}
   273  
   274  	ctx.Status(http.StatusCreated)
   275  }
   276  
   277  func DownloadPackageFile(ctx *context.Context) {
   278  	pfs, err := conda_model.SearchFiles(ctx, &conda_model.FileSearchOptions{
   279  		OwnerID:  ctx.Package.Owner.ID,
   280  		Channel:  ctx.Params("channel"),
   281  		Subdir:   ctx.Params("architecture"),
   282  		Filename: ctx.Params("filename"),
   283  	})
   284  	if err != nil {
   285  		apiError(ctx, http.StatusInternalServerError, err)
   286  		return
   287  	}
   288  
   289  	if len(pfs) != 1 {
   290  		apiError(ctx, http.StatusNotFound, nil)
   291  		return
   292  	}
   293  
   294  	pf := pfs[0]
   295  
   296  	s, u, _, err := packages_service.GetPackageFileStream(ctx, pf)
   297  	if err != nil {
   298  		apiError(ctx, http.StatusInternalServerError, err)
   299  		return
   300  	}
   301  
   302  	helper.ServePackageFile(ctx, s, u, pf)
   303  }