Add Cargo crate download endpoint
Summary
Adds the Cargo (Rust) crate download endpoint as part of the Cargo Package Manager MVC.
- New endpoint:
GET /api/v4/projects/:id/packages/cargo/:name/:version/download - New finder:
Packages::Cargo::PackageFinder— locates a Cargo package by normalized name + version, scoped to a project andinstallablestatuses - Registers
i_package_cargo_userandi_package_cargo_deploy_tokenHLL counters and metric YAMLs sotrack_package_event('pull_package', :cargo, ...)does not raise
All changes remain behind the existing package_registry_cargo_support WIP feature flag (disabled by default), so no changelog entry is included.
Plan context
This is MR 1 of 5 planned for the remaining work in the Cargo MVC. The download endpoint is mechanically equivalent to the RubyGems gems/:file_name download. The full MR breakdown for the remaining work (sparse index, upload authorize, upload publish, GA hardening) is here: #33060 (comment 3360481087)
How the URL is wired
The Cargo CLI fetches config.json (already implemented in !181281 (merged)), which currently returns the same project URL for both dl and api. Per the Cargo spec, the default dl template is {crate}/{version}/download — so the CLI will hit this endpoint as /api/v4/projects/:id/packages/cargo/:name/:version/download.
Test plan
-
bundle exec rspec spec/finders/packages/cargo/package_finder_spec.rb spec/requests/api/cargo_project_packages_spec.rb— 35 examples, 0 failures locally -
bundle exec rubocop— clean -
bin/rake gitlab:openapi:v2:generate gitlab:openapi:v3:generate— committed - Manual smoke test with the Cargo CLI once a
.cratefile is seeded in a project (deferred to after MR 4 lands)
Query plan
Validated against Postgres.ai (gitlab-production-main, Postgres 17.5) with a seeded cargo row in project 278964.
Query:
SELECT packages_packages.*
FROM packages_packages
INNER JOIN packages_cargo_metadata
ON packages_cargo_metadata.package_id = packages_packages.id
WHERE packages_packages.package_type = 15
AND packages_packages.project_id = 278964
AND packages_packages.status IN (0, 1, 5)
AND packages_cargo_metadata.project_id = 278964
AND packages_cargo_metadata.normalized_name = 'cargo-explain-probe'
AND packages_cargo_metadata.normalized_version = '1.0.0'
ORDER BY packages_packages.id DESC
LIMIT 1;EXPLAIN (ANALYZE, BUFFERS):
Limit (cost=6.65..6.65 rows=1 width=124) (actual time=0.124..0.126 rows=1 loops=1)
Buffers: shared hit=15
-> Sort (cost=6.65..6.65 rows=1 width=124) (actual time=0.123..0.124 rows=1 loops=1)
Sort Key: packages_packages.id DESC
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=15
-> Nested Loop (cost=0.58..6.64 rows=1 width=124) (actual time=0.081..0.082 rows=1 loops=1)
Buffers: shared hit=12
-> Index Scan using index_packages_packages_on_project_id_and_package_type on public.packages_packages (cost=0.44..3.46 rows=1 width=124) (actual time=0.057..0.058 rows=1 loops=1)
Index Cond: ((packages_packages.project_id = 278964) AND (packages_packages.package_type = 15))
Filter: (packages_packages.status = ANY ('{0,1,5}'::integer[]))
Rows Removed by Filter: 0
Buffers: shared hit=7
-> Index Scan using index_cargo_metadata_on_project_normalized_name_version on public.packages_cargo_metadata (cost=0.14..3.16 rows=1 width=8) (actual time=0.021..0.021 rows=1 loops=1)
Index Cond: ((packages_cargo_metadata.project_id = 278964) AND (packages_cargo_metadata.normalized_name = 'cargo-explain-probe'::text) AND (packages_cargo_metadata.normalized_version = '1.0.0'::text))
Buffers: shared hit=5
Settings: work_mem = '230MB', seq_page_cost = '4', effective_cache_size = '472585MB', jit = 'off', random_page_cost = '1.5'Both sides hit indexes; the metadata Index Cond uses all three columns of index_cargo_metadata_on_project_normalized_name_version. ~0.13 ms total, 15 shared buffer hits, no disk reads.
References
- Parent issue: #33060
- Feature flag rollout: #525330
- Prior MR (services + worker): !207060 (merged)
- Prior MR (DB + schema): !197846 (merged)
- Prior MR (API skeleton + config.json): !181281 (merged)