Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/8097.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a `sha256sums` option to file repositories that publishes SHA256SUMS files alongside the content, either one at the root of the repository or one in every directory. Each file lists every file below it in the format read by `sha256sum -c`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django 5.2.14 on 2026-09-23 08:33

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("file", "0020_filerepository_last_sync_details"),
]

operations = [
migrations.AddField(
model_name="filepublication",
name="sha256sums",
field=models.TextField(
choices=[
("disabled", "Do not generate SHA256SUMS files."),
(
"root",
"Generate a single SHA256SUMS at the root of the repository, listing every file by its path relative to the root.",
),
(
"directory",
"Generate a SHA256SUMS in every directory of the repository, each listing every file below it by its path relative to that directory.",
),
],
default="disabled",
),
),
migrations.AddField(
model_name="filerepository",
name="sha256sums",
field=models.TextField(
choices=[
("disabled", "Do not generate SHA256SUMS files."),
(
"root",
"Generate a single SHA256SUMS at the root of the repository, listing every file by its path relative to the root.",
),
(
"directory",
"Generate a SHA256SUMS in every directory of the repository, each listing every file below it by its path relative to that directory.",
),
],
default="disabled",
),
),
]
24 changes: 24 additions & 0 deletions pulp_file/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,27 @@ class FileRepository(Repository, AutoAddObjPermsMixin):
CONTENT_TYPES = [FileContent]
REMOTE_TYPES = [FileRemote, FileGitRemote]

# Constants for the ChoiceField 'sha256sums'
SHA256SUMS_DISABLED = "disabled"
SHA256SUMS_ROOT = "root"
SHA256SUMS_DIRECTORY = "directory"

SHA256SUMS_CHOICES = (
(SHA256SUMS_DISABLED, "Do not generate SHA256SUMS files."),
(
SHA256SUMS_ROOT,
"Generate a single SHA256SUMS at the root of the repository, listing every file "
"by its path relative to the root.",
),
(
SHA256SUMS_DIRECTORY,
"Generate a SHA256SUMS in every directory of the repository, each listing every "
"file below it by its path relative to that directory.",
),
)

manifest = models.TextField(default="PULP_MANIFEST", null=True)
sha256sums = models.TextField(choices=SHA256SUMS_CHOICES, default=SHA256SUMS_DISABLED)
autopublish = models.BooleanField(default=False)
last_sync_details = models.JSONField(default=dict)

Expand Down Expand Up @@ -117,6 +137,7 @@ def on_new_version(self, version):
if self.autopublish:
tasks.publish(
manifest=self.manifest,
sha256sums=self.sha256sums,
repository_version_pk=version.pk,
)

Expand Down Expand Up @@ -144,6 +165,9 @@ class FilePublication(Publication, AutoAddObjPermsMixin):
TYPE = "file"

manifest = models.TextField(null=True)
sha256sums = models.TextField(
choices=FileRepository.SHA256SUMS_CHOICES, default=FileRepository.SHA256SUMS_DISABLED
)

class Meta:
default_related_name = "%(app_label)s_%(model_name)s"
Expand Down
64 changes: 62 additions & 2 deletions pulp_file/app/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@
FileRemote,
FileRepository,
)
from pulp_file.manifest import Sha256Sums


def validate_manifest_sha256sums(manifest, sha256sums):
"""
Validate that the manifest filename does not collide with a generated SHA256SUMS file.

Raises:
serializers.ValidationError: If the manifest would be published at a generated path.

"""
if sha256sums != FileRepository.SHA256SUMS_DISABLED and manifest == Sha256Sums.FILENAME:
raise serializers.ValidationError(
_("The manifest cannot be named {name} while SHA256SUMS files are generated.").format(
name=Sha256Sums.FILENAME
)
)


class FileContentSerializer(SingleArtifactContentUploadSerializer, ContentChecksumSerializer):
Expand Down Expand Up @@ -143,13 +160,43 @@ class FileRepositorySerializer(RepositorySerializer):
allow_null=True,
)

sha256sums = serializers.ChoiceField(
help_text=_(
"Whether to generate SHA256SUMS files, and where to place them. 'root' writes a "
"single file at the root of the repository, 'directory' writes one in every "
"directory. Each file lists every file below it, with paths relative to itself."
),
choices=FileRepository.SHA256SUMS_CHOICES,
default=FileRepository.SHA256SUMS_DISABLED,
required=False,
)

last_sync_details = serializers.JSONField(
help_text=_("Details about the last sync of this repository."),
read_only=True,
)

def validate(self, data):
"""Validate the FileRepository data."""
data = super().validate(data)

validate_manifest_sha256sums(
data.get("manifest", getattr(self.instance, "manifest", None)),
data.get(
"sha256sums",
getattr(self.instance, "sha256sums", FileRepository.SHA256SUMS_DISABLED),
),
)

return data

class Meta:
fields = RepositorySerializer.Meta.fields + ("autopublish", "manifest", "last_sync_details")
fields = RepositorySerializer.Meta.fields + (
"autopublish",
"manifest",
"sha256sums",
"last_sync_details",
)
model = FileRepository


Expand Down Expand Up @@ -218,11 +265,24 @@ class FilePublicationSerializer(PublicationSerializer):
required=False,
allow_null=True,
)
sha256sums = serializers.ChoiceField(
help_text=_(
"Whether to generate SHA256SUMS files, and where to place them. Defaults to the "
"setting on the repository being published."
),
choices=FileRepository.SHA256SUMS_CHOICES,
required=False,
)
checkpoint = serializers.BooleanField(required=False)

class Meta:
model = FilePublication
fields = PublicationSerializer.Meta.fields + ("distributions", "manifest", "checkpoint")
fields = PublicationSerializer.Meta.fields + (
"distributions",
"manifest",
"sha256sums",
"checkpoint",
)


class FileDistributionSerializer(DistributionSerializer):
Expand Down
35 changes: 29 additions & 6 deletions pulp_file/app/tasks/publishing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,65 @@
RepositoryVersion,
)

from pulp_file.app.models import FilePublication
from pulp_file.app.models import FilePublication, FileRepository
from pulp_file.app.serializers import FilePublicationSerializer
from pulp_file.manifest import Entry, Manifest
from pulp_file.manifest import Entry, Manifest, Sha256Sums

log = logging.getLogger(__name__)


def publish(manifest, repository_version_pk, checkpoint=False, **kwargs):
def publish(
manifest,
repository_version_pk,
sha256sums=FileRepository.SHA256SUMS_DISABLED,
checkpoint=False,
**kwargs,
):
"""
Create a Publication based on a RepositoryVersion.

Args:
manifest (str): Filename to use for manifest file.
repository_version_pk (str): Create a publication from this repository version.
sha256sums (str): Whether to generate SHA256SUMS files, and where to place them.
checkpoint (bool): Whether to create a checkpoint publication.

"""
repo_version = RepositoryVersion.objects.get(pk=repository_version_pk)

log.info(
_("Publishing: repository={repo}, version={ver}, manifest={manifest}").format(
repo=repo_version.repository.name, ver=repo_version.number, manifest=manifest
_(
"Publishing: repository={repo}, version={ver}, manifest={manifest}, "
"sha256sums={sha256sums}"
).format(
repo=repo_version.repository.name,
ver=repo_version.number,
manifest=manifest,
sha256sums=sha256sums,
)
)

with tempfile.TemporaryDirectory(dir="."):
with tempfile.TemporaryDirectory(dir=".") as temp_dir:
with FilePublication.create(
repo_version, pass_through=True, checkpoint=checkpoint
) as publication:
publication.manifest = manifest
publication.sha256sums = sha256sums
if manifest:
manifest = Manifest(manifest)
manifest.write(yield_entries_for_version(repo_version))
PublishedMetadata.create_from_file(
file=File(open(manifest.relative_path, "rb")), publication=publication
)
if sha256sums != FileRepository.SHA256SUMS_DISABLED:
sums = Sha256Sums(per_directory=sha256sums == FileRepository.SHA256SUMS_DIRECTORY)
written = sums.write(yield_entries_for_version(repo_version), temp_dir)
for relative_path, local_path in written.items():
PublishedMetadata.create_from_file(
file=File(open(local_path, "rb")),
relative_path=relative_path,
publication=publication,
)

log.info(_("Publication: {publication} created").format(publication=publication.pk))

Expand Down
11 changes: 10 additions & 1 deletion pulp_file/app/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
FileRemoteSerializer,
FileRepositorySerializer,
FileRepositorySyncURLSerializer,
validate_manifest_sha256sums,
)


Expand Down Expand Up @@ -574,8 +575,16 @@ def create(self, request, **kwargs):
repository_version = serializer.validated_data.get("repository_version")
manifest = serializer.validated_data.get("manifest")
checkpoint = serializer.validated_data.get("checkpoint")
sha256sums = serializer.validated_data.get("sha256sums")
if sha256sums is None:
sha256sums = repository_version.repository.cast().sha256sums
validate_manifest_sha256sums(manifest, sha256sums)

task_kwargs = {"repository_version_pk": str(repository_version.pk), "manifest": manifest}
task_kwargs = {
"repository_version_pk": str(repository_version.pk),
"manifest": manifest,
"sha256sums": sha256sums,
}

if checkpoint:
task_kwargs["checkpoint"] = True
Expand Down
30 changes: 30 additions & 0 deletions pulp_file/docs/user/guides/publish-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,33 @@ pulp file repository update --name $REPO_NAME --autopublish
# This configures the distribution to be track the latest repository version for a given repository
pulp file distribution update --name $DIST_NAME --repository $REPO_NAME
```

## Publish SHA256SUMS Files

A repository can publish `SHA256SUMS` files alongside its content, in the format read by
`sha256sum -c`. This is off by default. Setting it to `root` publishes a single file at the root of
the repository, and `directory` publishes one in every directory. Each file lists every file below
it, with paths relative to the file itself.

This option is not exposed by the CLI yet, so set it through the API:

```bash
REPO_HREF=$(pulp file repository show --name $REPO_NAME | jq -r '.pulp_href')

# Every new publication of this repository gets a SHA256SUMS in each of its directories
http PATCH :$REPO_HREF sha256sums=directory
```

A publication inherits the setting from the repository it is published from, and can override it:

```bash
http POST :/pulp/api/v3/publications/file/file/ repository=$REPO_HREF sha256sums=root
```

Consumers verify what they downloaded by running `sha256sum -c SHA256SUMS` in the directory they
downloaded it into. Content that Pulp has not downloaded yet has no digest to publish and is left
out.

The generated files take the `SHA256SUMS` paths for themselves, so the manifest cannot be named
`SHA256SUMS` while they are generated, and a repository already holding content at one of those
paths cannot be published.
Loading
Loading