1919 BaseModel ,
2020 Content ,
2121 Distribution ,
22+ ProgressReport ,
2223 Publication ,
2324 Remote ,
2425 Repository ,
@@ -394,6 +395,7 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
394395
395396 autopublish = models .BooleanField (default = False )
396397 allow_package_substitution = models .BooleanField (default = True )
398+ error_on_reject = models .BooleanField (default = True )
397399
398400 class Meta :
399401 default_related_name = "%(app_label)s_%(model_name)s"
@@ -424,7 +426,8 @@ def finalize_new_version(self, new_version):
424426 Remove duplicate packages that have the same filename.
425427
426428 When allow_package_substitution is False, reject any new version that would implicitly
427- replace existing content with different checksums (content substitution).
429+ replace existing content with different checksums (content substitution), unless
430+ error_on_reject is False, in which case the conflicting packages are skipped.
428431
429432 Also checks newly added content against the repository's blocklist entries.
430433 """
@@ -436,53 +439,126 @@ def finalize_new_version(self, new_version):
436439
437440 def _check_for_package_substitution (self , new_version ):
438441 """
439- Raise a ValidationError if newly added packages would replace existing packages
440- that have the same filename but a different sha256 checksum.
442+ Handle packages that would replace existing packages with the same filename but a
443+ different sha256 checksum.
444+
445+ When error_on_reject is True, raise a ValidationError. When False, remove the
446+ newly added conflicting packages from the version and record them in a progress report.
441447 """
442448 qs = PythonPackageContent .objects .filter (pk__in = new_version .content )
443449 duplicates = collect_duplicates (qs , ("filename" ,))
444- if duplicates :
450+ if not duplicates :
451+ return
452+
453+ if self .error_on_reject :
445454 raise ValidationError (
446455 "Found duplicate packages being added with the same filename but different "
447456 "checksums. To allow this, set 'allow_package_substitution' to True on the "
448457 f"repository. Conflicting packages: { duplicates } "
449458 )
450459
460+ added_content = PythonPackageContent .objects .filter (
461+ pk__in = new_version .added (base_version = new_version .base_version )
462+ )
463+ added_pks = {
464+ str (pkg .pk ): pkg .filename
465+ for pkg in added_content .only ("pk" , "filename" )
466+ }
467+ to_remove_pks = []
468+ messages = []
469+ for dup in duplicates :
470+ for pk in dup .duplicate_pks :
471+ if pk in added_pks :
472+ to_remove_pks .append (pk )
473+ messages .append (f"{ added_pks [pk ]} ({ pk } )" )
474+
475+ if to_remove_pks :
476+ new_version .remove_content (PythonPackageContent .objects .filter (pk__in = to_remove_pks ))
477+ self ._report_rejected_packages (
478+ messages ,
479+ message = "Skipping packages rejected by package substitution policy" ,
480+ code = "python.reject.substitution" ,
481+ )
482+
451483 def _check_blocklist (self , new_version ):
452484 """
453485 Check newly added content in a repository version against the blocklist.
486+
487+ When error_on_reject is True, raise a ValidationError. When False, remove the
488+ blocklisted packages from the version and record them in a progress report.
454489 """
455490 added_content = PythonPackageContent .objects .filter (
456- pk__in = new_version .added (). values_list ( "pk" , flat = True )
457- ).only ("filename" , "name_normalized" , "version" )
458- if added_content .exists ():
459- self . check_blocklist_for_packages ( added_content )
491+ pk__in = new_version .added (base_version = new_version . base_version )
492+ ).only ("pk" , " filename" , "name_normalized" , "version" )
493+ if not added_content .exists ():
494+ return
460495
461- def check_blocklist_for_packages (self , packages ):
496+ blocked = self .find_blocklisted_packages (added_content )
497+ if not blocked :
498+ return
499+
500+ if self .error_on_reject :
501+ raise ValidationError (
502+ "Blocklisted packages cannot be added to this repository: {}" .format (
503+ ", " .join (pkg .filename for pkg in blocked )
504+ )
505+ )
506+
507+ new_version .remove_content (
508+ PythonPackageContent .objects .filter (pk__in = [p .pk for p in blocked ])
509+ )
510+ self ._report_rejected_packages (
511+ [pkg .filename for pkg in blocked ],
512+ message = "Skipping packages rejected by blocklist policy" ,
513+ code = "python.reject.blocklist" ,
514+ )
515+
516+ def find_blocklisted_packages (self , packages ):
462517 """
463- Raise a ValidationError if any of the given packages match a blocklist entry.
518+ Return the packages from `` packages`` that match a blocklist entry.
464519 """
465- entries = PythonBlocklistEntry .objects .filter (repository = self )
466- if not entries . exists () :
467- return
520+ entries = list ( PythonBlocklistEntry .objects .filter (repository = self ) )
521+ if not entries :
522+ return []
468523
469524 blocked = []
470525 for pkg in packages :
471526 for entry in entries :
472527 if entry .filename and entry .filename == pkg .filename :
473- blocked .append (pkg . filename )
528+ blocked .append (pkg )
474529 break
475530 if entry .name == pkg .name_normalized :
476531 if not entry .version or entry .version == pkg .version :
477- blocked .append (pkg . filename )
532+ blocked .append (pkg )
478533 break
534+ return blocked
535+
536+ def check_blocklist_for_packages (self , packages ):
537+ """
538+ Raise a ValidationError if any of the given packages match a blocklist entry.
539+ """
540+ blocked = self .find_blocklisted_packages (packages )
479541 if blocked :
480542 raise ValidationError (
481543 "Blocklisted packages cannot be added to this repository: {}" .format (
482- ", " .join (blocked )
544+ ", " .join (pkg . filename for pkg in blocked )
483545 )
484546 )
485547
548+ def _report_rejected_packages (self , details , message , code ):
549+ """
550+ Record skipped packages in a task progress report.
551+ """
552+ suffix = "; " .join (details )
553+ log .info ("%s: %s" , message , suffix )
554+ with ProgressReport (
555+ message = message ,
556+ code = code ,
557+ total = len (details ),
558+ suffix = suffix ,
559+ ) as pb :
560+ pb .increase_by (len (details ))
561+
486562
487563class PythonBlocklistEntry (BaseModel ):
488564 """
0 commit comments