11import hashlib
22import struct
33
4+ from django .db import IntegrityError
5+
46from pulpcore .plugin .models import Artifact , ContentArtifact
57from pulpcore .plugin .tasking import aadd_and_remove
68
79from pulp_rust .app .models import RustContent , RustDependency , RustRepository
8- from pulp_rust .app .utils import extract_cargo_toml , extract_dependencies
10+ from pulp_rust .app .utils import (
11+ canonicalize_crate_name ,
12+ extract_cargo_toml ,
13+ extract_dependencies ,
14+ strip_semver_build_metadata ,
15+ )
916
1017
1118def parse_cargo_publish_body (body ):
@@ -55,15 +62,19 @@ async def apublish_package(repository_pk, metadata, crate_path):
5562 """
5663 repository = await RustRepository .objects .aget (pk = repository_pk )
5764
58- # Create the artifact from the .crate file
65+ # Create the artifact from the .crate file, or reuse an existing one
66+ # with the same checksum (Artifact has a unique constraint on digests).
5967 with open (crate_path , "rb" ) as f :
6068 cksum = hashlib .sha256 (f .read ()).hexdigest ()
6169
6270 artifact = Artifact .init_and_validate (crate_path , expected_digests = {"sha256" : cksum })
63- await artifact .asave ()
71+ try :
72+ await artifact .asave ()
73+ except IntegrityError :
74+ artifact = await Artifact .objects .aget (sha256 = cksum )
6475
6576 # Extract authoritative metadata from the Cargo.toml inside the .crate tarball.
66- # The publish JSON metadata is NOT authoritative — a rogue client can send metadata
77+ # The publish JSON metadata is NOT authoritative - a rogue client can send metadata
6778 # that doesn't match the actual package. We only use the JSON name/vers to locate the
6879 # Cargo.toml within the tarball, then extract everything from the Cargo.toml itself.
6980 # See: https://github.com/rust-lang/cargo/issues/14492
@@ -72,34 +83,52 @@ async def apublish_package(repository_pk, metadata, crate_path):
7283 package = cargo_toml .get ("package" , {})
7384
7485 name = package ["name" ]
75- vers = package ["version" ]
86+ canonical_name = canonicalize_crate_name (name )
87+ # Strip build metadata - SemVer 2.0.0 treats versions differing only in
88+ # build metadata as identical, and the index must not contain duplicates.
89+ vers = strip_semver_build_metadata (package ["version" ])
7690
7791 # Build dependency list from the Cargo.toml (authoritative source)
7892 deps = extract_dependencies (cargo_toml )
7993
80- # Create the content record
81- content = RustContent (
94+ # Reuse existing content if it already exists in the domain with the same
95+ # checksum (e.g. from a pull-through cache or another repository's publish).
96+ # Content in Pulp is globally shared - the same object can belong to
97+ # multiple repositories. Including cksum in the lookup allows different
98+ # crates with the same name+version (e.g. a private crate shadowing a
99+ # public one) to coexist as separate content objects within a domain.
100+ content = await RustContent .objects .filter (
82101 name = name ,
83102 vers = vers ,
84103 cksum = cksum ,
85- features = cargo_toml .get ("features" , {}),
86- features2 = None ,
87- links = package .get ("links" ),
88- rust_version = package .get ("rust-version" ),
89104 _pulp_domain_id = repository .pulp_domain_id ,
90- )
91- await content .asave ()
92-
93- # Create dependencies
94- if deps :
95- await RustDependency .objects .abulk_create (
96- [RustDependency (content = content , ** dep ) for dep in deps ]
105+ ).afirst ()
106+
107+ if content is None :
108+ content = RustContent (
109+ name = name ,
110+ canonical_name = canonical_name ,
111+ vers = vers ,
112+ cksum = cksum ,
113+ features = cargo_toml .get ("features" , {}),
114+ features2 = None ,
115+ links = package .get ("links" ),
116+ rust_version = package .get ("rust-version" ),
117+ _pulp_domain_id = repository .pulp_domain_id ,
97118 )
119+ await content .asave ()
120+
121+ if deps :
122+ await RustDependency .objects .abulk_create (
123+ [RustDependency (content = content , ** dep ) for dep in deps ]
124+ )
98125
99- # Create the content artifact (links the .crate file to the content)
126+ # Create the content artifact if it doesn't already exist
100127 relative_path = f"{ name } /{ name } -{ vers } .crate"
101- await ContentArtifact .objects .acreate (
102- artifact = artifact , content = content , relative_path = relative_path
128+ await ContentArtifact .objects .aget_or_create (
129+ content = content ,
130+ relative_path = relative_path ,
131+ defaults = {"artifact" : artifact },
103132 )
104133
105134 # Add the content to a new repository version
0 commit comments