11import json
22import logging
3+ import urllib .request
4+ import urllib .error
35
46from rest_framework .views import APIView
57from rest_framework .viewsets import ViewSet
1517)
1618from drf_spectacular .utils import extend_schema
1719from dynaconf import settings
18- from pathlib import PurePath
1920from urllib .parse import urljoin
2021
2122from pulpcore .plugin .util import get_domain
2223
23- from pulp_rust .app .models import RustDistribution , RustRepository , RustContent
24+ from pulp_rust .app .models import RustDistribution , RustContent , _strip_sparse_prefix
2425from pulp_rust .app .serializers import (
2526 IndexRootSerializer ,
2627 RustContentSerializer ,
@@ -85,7 +86,6 @@ def initial(self, request, *args, **kwargs):
8586 """Perform common initialization tasks for API endpoints."""
8687 super ().initial (request , * args , ** kwargs )
8788 domain_name = get_domain ().name
88- log .warning (self .kwargs )
8989 repo = self .kwargs ["repo" ]
9090 if settings .DOMAIN_ENABLED :
9191 self .base_content_url = urljoin (BASE_CONTENT_URL , f"pulp/cargo/{ domain_name } /{ repo } /" )
@@ -121,7 +121,7 @@ class CargoIndexApiViewSet(ApiMixin, ViewSet):
121121 responses = {200 : RustContentSerializer },
122122 summary = "Get package metadata" ,
123123 )
124- def retrieve (self , request , path ):
124+ def retrieve (self , request , path , ** kwargs ):
125125 """
126126 Retrieve crate metadata for the sparse protocol.
127127
@@ -132,42 +132,57 @@ def retrieve(self, request, path):
132132 - 4+ chars: {first-two}/{second-two}/{crate}
133133
134134 Returns newline-delimited JSON, one version per line.
135+
136+ If the crate is not found locally and the distribution has a remote,
137+ the metadata is proxied from the upstream sparse index.
135138 """
136139 repo_ver , content = self .get_rvc ()
137140
138- if content is None :
139- return HttpResponseNotFound ("No content available" )
140-
141- # Extract crate name from the path
142- meta_path = PurePath (path )
143- crate_name = meta_path .name .lower ()
141+ # Extract crate name from the path (last component)
142+ crate_name = path .rsplit ("/" , 1 )[- 1 ].lower ()
144143
145- # Query for all versions of this crate
146- crate_versions = content .filter (name = crate_name ).order_by ("vers" )
144+ # Try to serve from local content first
145+ if content is not None :
146+ crate_versions = content .filter (name = crate_name ).order_by ("vers" )
147+ if crate_versions .exists ():
148+ return self ._build_index_response (crate_versions )
147149
148- if not crate_versions .exists ():
149- return HttpResponseNotFound (f"Crate '{ crate_name } ' not found" )
150+ # Fall back to proxying from the upstream remote
151+ if self .distribution .remote :
152+ remote = self .distribution .remote .cast ()
153+ index_url = _strip_sparse_prefix (remote .url ).rstrip ("/" )
154+ upstream_url = f"{ index_url } /{ path } "
155+ try :
156+ response = urllib .request .urlopen (upstream_url )
157+ return HttpResponse (response .read (), content_type = "text/plain" )
158+ except urllib .error .HTTPError as e :
159+ if e .code == 404 :
160+ return HttpResponseNotFound (f"Crate '{ crate_name } ' not found" )
161+ raise
162+
163+ return HttpResponseNotFound (f"Crate '{ crate_name } ' not found" )
150164
151- # Build newline-delimited JSON response
165+ @staticmethod
166+ def _build_index_response (crate_versions ):
167+ """Build a newline-delimited JSON response from local crate versions."""
152168 lines = []
153169 for crate_version in crate_versions :
154- # Fetch dependencies for this version
155170 deps = []
156171 for dep in crate_version .dependencies .all ():
157- dep_obj = {
158- "name" : dep . name ,
159- "req " : dep .req ,
160- "features " : dep .features ,
161- "optional " : dep .optional ,
162- "default_features " : dep .default_features ,
163- "target " : dep .target ,
164- "kind " : dep .kind ,
165- "registry " : dep .registry ,
166- "package " : dep .package ,
167- }
168- deps . append ( dep_obj )
169-
170- # Build the version object according to sparse protocol
172+ deps . append (
173+ {
174+ "name " : dep .name ,
175+ "req " : dep .req ,
176+ "features " : dep .features ,
177+ "optional " : dep .optional ,
178+ "default_features " : dep .default_features ,
179+ "target " : dep .target ,
180+ "kind " : dep .kind ,
181+ "registry " : dep .registry ,
182+ "package" : dep . package ,
183+ }
184+ )
185+
171186 version_obj = {
172187 "name" : crate_version .name ,
173188 "vers" : crate_version .vers ,
@@ -179,18 +194,14 @@ def retrieve(self, request, path):
179194 "v" : crate_version .v ,
180195 }
181196
182- # Add optional fields only if present
183197 if crate_version .features2 :
184198 version_obj ["features2" ] = crate_version .features2
185199 if crate_version .rust_version :
186200 version_obj ["rust_version" ] = crate_version .rust_version
187201
188- # Serialize to JSON and add to lines
189202 lines .append (json .dumps (version_obj ))
190203
191- # Join with newlines and return as plain text
192- response_text = "\n " .join (lines )
193- return HttpResponse (response_text , content_type = "text/plain" )
204+ return HttpResponse ("\n " .join (lines ), content_type = "text/plain" )
194205
195206
196207class IndexRoot (ApiMixin , ViewSet ):
@@ -221,14 +232,9 @@ def retrieve(self, request, repo):
221232
222233class CargoDownloadApiView (APIView ):
223234 """
224- ViewSet for interacting with Cargo's API API
235+ View for Cargo's crate download, readme, yank, and unyank endpoints.
225236 """
226237
227- model = RustRepository
228- queryset = RustRepository .objects .all ()
229-
230- lookup_field = "name"
231-
232238 # Authentication disabled for now
233239 authentication_classes = []
234240 permission_classes = []
@@ -248,23 +254,50 @@ def redirect_to_content_app(self, distribution, relative_path, request):
248254 f"{ self .get_full_path (distribution .base_path )} /{ relative_path } "
249255 )
250256
251- def get_repository_and_distributions (self , name ):
252- repository = get_object_or_404 (RustRepository , name = name , pulp_domain = get_domain ())
253- distribution = get_object_or_404 (
254- RustDistribution , repository = repository , pulp_domain = get_domain ()
257+ def get_distribution (self ):
258+ return get_object_or_404 (
259+ RustDistribution , base_path = self .kwargs ["repo" ], pulp_domain = get_domain ()
255260 )
256- return repository , distribution
257261
258- def get (self , request , name , version ):
262+ def get (self , request , name , version , rest , ** kwargs ):
259263 """
260- Responds to GET requests about packages by reference
264+ Responds to GET requests for crate downloads and readmes.
265+
266+ Handles:
267+ - api/v1/crates/{name}/{version}/download - redirect to .crate file
268+ - api/v1/crates/{name}/{version}/readme - not yet implemented
261269 """
262- repo , distro = self .get_repository_and_distributions (name )
263- content = get_object_or_404 (
264- RustContent , name = name , vers = version , pk__in = repo .latest_version ().content
265- )
266- relative_path = content .contentartifact_set .get ().relative_path
267- return self .redirect_to_content_app (distro , relative_path , request )
270+ distro = self .get_distribution ()
271+
272+ if rest == "download" :
273+ relative_path = f"{ name } /{ name } -{ version } .crate"
274+ return self .redirect_to_content_app (distro , relative_path , request )
275+ elif rest == "readme" :
276+ raise NotImplementedError ("Readme endpoint is not yet implemented" )
277+ else :
278+ raise Http404 (f"Unknown action: { rest } " )
279+
280+ def delete (self , request , name , version , rest , ** kwargs ):
281+ """
282+ Responds to DELETE requests for yanking crate versions.
283+
284+ Handles:
285+ - api/v1/crates/{name}/{version}/yank
286+ """
287+ if rest != "yank" :
288+ raise Http404 (f"Unknown action: { rest } " )
289+ raise NotImplementedError ("Yank endpoint is not yet implemented" )
290+
291+ def put (self , request , name , version , rest , ** kwargs ):
292+ """
293+ Responds to PUT requests for unyanking crate versions.
294+
295+ Handles:
296+ - api/v1/crates/{name}/{version}/unyank
297+ """
298+ if rest != "unyank" :
299+ raise Http404 (f"Unknown action: { rest } " )
300+ raise NotImplementedError ("Unyank endpoint is not yet implemented" )
268301
269302
270303def has_task_completed (task ):
0 commit comments