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  }