|
| 1 | +import hashlib |
| 2 | +import struct |
| 3 | +import tempfile |
| 4 | + |
| 5 | +from pulpcore.plugin.models import Artifact, ContentArtifact |
| 6 | +from pulpcore.plugin.tasking import aadd_and_remove |
| 7 | + |
| 8 | +from pulp_rust.app.models import RustContent, RustDependency, RustRepository |
| 9 | +from pulp_rust.app.utils import extract_cargo_toml, extract_dependencies |
| 10 | + |
| 11 | + |
| 12 | +def parse_cargo_publish_body(body): |
| 13 | + """ |
| 14 | + Parse the binary request body from ``cargo publish``. |
| 15 | +
|
| 16 | + Format (per https://doc.rust-lang.org/cargo/reference/registry-web-api.html#publish): |
| 17 | + 4 bytes: JSON metadata length (little-endian u32) |
| 18 | + N bytes: JSON metadata (UTF-8) |
| 19 | + 4 bytes: .crate file length (little-endian u32) |
| 20 | + M bytes: .crate file (binary) |
| 21 | +
|
| 22 | + Returns: |
| 23 | + (metadata_dict, crate_bytes) |
| 24 | + """ |
| 25 | + import json |
| 26 | + |
| 27 | + offset = 0 |
| 28 | + |
| 29 | + json_len = struct.unpack_from("<I", body, offset)[0] |
| 30 | + offset += 4 |
| 31 | + |
| 32 | + json_bytes = body[offset : offset + json_len] |
| 33 | + offset += json_len |
| 34 | + metadata = json.loads(json_bytes) |
| 35 | + |
| 36 | + crate_len = struct.unpack_from("<I", body, offset)[0] |
| 37 | + offset += 4 |
| 38 | + |
| 39 | + crate_bytes = body[offset : offset + crate_len] |
| 40 | + offset += crate_len |
| 41 | + |
| 42 | + return metadata, crate_bytes |
| 43 | + |
| 44 | + |
| 45 | +async def apublish_package(repository_pk, metadata, crate_bytes): |
| 46 | + """ |
| 47 | + Publish a crate to a repository. |
| 48 | +
|
| 49 | + Creates the Artifact, RustContent, ContentArtifact, and RustDependency records, |
| 50 | + then adds the content to a new repository version. |
| 51 | +
|
| 52 | + Args: |
| 53 | + repository_pk: Primary key of the target repository. |
| 54 | + metadata: Parsed JSON metadata from the cargo publish request. |
| 55 | + crate_bytes: Raw bytes of the .crate tarball. |
| 56 | + """ |
| 57 | + repository = await RustRepository.objects.aget(pk=repository_pk) |
| 58 | + |
| 59 | + name = metadata["name"] |
| 60 | + vers = metadata["vers"] |
| 61 | + |
| 62 | + # Create the artifact from the .crate bytes |
| 63 | + cksum = hashlib.sha256(crate_bytes).hexdigest() |
| 64 | + |
| 65 | + with tempfile.NamedTemporaryFile(suffix=".crate", delete=False) as tmp: |
| 66 | + tmp.write(crate_bytes) |
| 67 | + tmp.flush() |
| 68 | + artifact = Artifact.init_and_validate(tmp.name, expected_digests={"sha256": cksum}) |
| 69 | + await artifact.asave() |
| 70 | + |
| 71 | + # Extract metadata from the Cargo.toml inside the .crate tarball. |
| 72 | + # Used as a fallback for fields not present in the publish JSON. |
| 73 | + cargo_toml = extract_cargo_toml(artifact.file.path, name, vers) |
| 74 | + |
| 75 | + # Build dependency list from the publish metadata. |
| 76 | + # The publish JSON uses "version_req" (not "req") and "explicit_name_in_toml" (not "package") |
| 77 | + # per the Cargo registry web API spec. |
| 78 | + deps = [] |
| 79 | + for dep in metadata.get("deps", []): |
| 80 | + deps.append( |
| 81 | + { |
| 82 | + "name": dep["name"], |
| 83 | + "req": dep.get("version_req", dep.get("req", "*")), |
| 84 | + "features": dep.get("features", []), |
| 85 | + "optional": dep.get("optional", False), |
| 86 | + "default_features": dep.get("default_features", True), |
| 87 | + "target": dep.get("target"), |
| 88 | + "kind": dep.get("kind", "normal"), |
| 89 | + "registry": dep.get("registry"), |
| 90 | + "package": dep.get("explicit_name_in_toml") or dep.get("package"), |
| 91 | + } |
| 92 | + ) |
| 93 | + |
| 94 | + # Fallback to Cargo.toml-parsed deps if publish metadata had none |
| 95 | + if not deps: |
| 96 | + deps = extract_dependencies(cargo_toml) |
| 97 | + |
| 98 | + # Create the content record |
| 99 | + content = RustContent( |
| 100 | + name=name, |
| 101 | + vers=vers, |
| 102 | + cksum=cksum, |
| 103 | + features=metadata.get("features", cargo_toml.get("features", {})), |
| 104 | + features2=metadata.get("features2"), |
| 105 | + links=metadata.get("links", cargo_toml.get("package", {}).get("links")), |
| 106 | + rust_version=metadata.get( |
| 107 | + "rust_version", cargo_toml.get("package", {}).get("rust-version") |
| 108 | + ), |
| 109 | + _pulp_domain_id=repository.pulp_domain_id, |
| 110 | + ) |
| 111 | + await content.asave() |
| 112 | + |
| 113 | + # Create dependencies |
| 114 | + if deps: |
| 115 | + await RustDependency.objects.abulk_create( |
| 116 | + [RustDependency(content=content, **dep) for dep in deps] |
| 117 | + ) |
| 118 | + |
| 119 | + # Create the content artifact (links the .crate file to the content) |
| 120 | + relative_path = f"{name}/{name}-{vers}.crate" |
| 121 | + await ContentArtifact.objects.acreate( |
| 122 | + artifact=artifact, content=content, relative_path=relative_path |
| 123 | + ) |
| 124 | + |
| 125 | + # Add the content to a new repository version |
| 126 | + await aadd_and_remove( |
| 127 | + repository_pk=repository.pk, |
| 128 | + add_content_units=[content.pk], |
| 129 | + remove_content_units=[], |
| 130 | + ) |
0 commit comments