From c16cee0fb3e294049623b5fb9c2ddbc1ab950cb7 Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:12:32 +0100 Subject: [PATCH 1/8] Add checksum field to location in database and GraphQL --- thoth-api/migrations/20260429_v1.2.0/down.sql | 2 + thoth-api/migrations/20260429_v1.2.0/up.sql | 2 + thoth-api/src/graphql/model.rs | 5 + thoth-api/src/graphql/mutation.rs | 8 +- thoth-api/src/graphql/tests.rs | 2 + thoth-api/src/model/file/crud.rs | 6 + thoth-api/src/model/file/tests.rs | 4 +- thoth-api/src/model/location/crud.rs | 3 + thoth-api/src/model/location/mod.rs | 5 + thoth-api/src/model/location/policy.rs | 16 +++ thoth-api/src/model/location/tests.rs | 132 ++++++++++++++++++ thoth-api/src/schema.rs | 1 + thoth-errors/src/lib.rs | 4 + 13 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 thoth-api/migrations/20260429_v1.2.0/down.sql create mode 100644 thoth-api/migrations/20260429_v1.2.0/up.sql diff --git a/thoth-api/migrations/20260429_v1.2.0/down.sql b/thoth-api/migrations/20260429_v1.2.0/down.sql new file mode 100644 index 000000000..b94303f88 --- /dev/null +++ b/thoth-api/migrations/20260429_v1.2.0/down.sql @@ -0,0 +1,2 @@ +ALTER TABLE public.location + DROP COLUMN IF EXISTS checksum; diff --git a/thoth-api/migrations/20260429_v1.2.0/up.sql b/thoth-api/migrations/20260429_v1.2.0/up.sql new file mode 100644 index 000000000..b1fc82d44 --- /dev/null +++ b/thoth-api/migrations/20260429_v1.2.0/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE public.location + ADD COLUMN checksum TEXT; diff --git a/thoth-api/src/graphql/model.rs b/thoth-api/src/graphql/model.rs index 974e8762a..098b15c18 100644 --- a/thoth-api/src/graphql/model.rs +++ b/thoth-api/src/graphql/model.rs @@ -1916,6 +1916,11 @@ impl Location { self.canonical } + #[graphql(description = "Checksum of the full text file as returned by the platform")] + pub fn checksum(&self) -> Option<&String> { + self.checksum.as_ref() + } + #[graphql(description = "Date and time at which the location record was created")] pub fn created_at(&self) -> Timestamp { self.created_at diff --git a/thoth-api/src/graphql/mutation.rs b/thoth-api/src/graphql/mutation.rs index fe698e582..33086dbfa 100644 --- a/thoth-api/src/graphql/mutation.rs +++ b/thoth-api/src/graphql/mutation.rs @@ -1488,7 +1488,13 @@ impl MutationRoot { &mime_type, bytes, )?; - file_upload.sync_related_metadata(context, &work, &cdn_url, featured_video_dimensions)?; + file_upload.sync_related_metadata( + context, + &work, + &cdn_url, + &file.sha256, + featured_video_dimensions, + )?; reconcile_replaced_object( s3_client, diff --git a/thoth-api/src/graphql/tests.rs b/thoth-api/src/graphql/tests.rs index 4965025a8..f5b283c1c 100644 --- a/thoth-api/src/graphql/tests.rs +++ b/thoth-api/src/graphql/tests.rs @@ -470,6 +470,7 @@ fn make_new_location(publication_id: Uuid, canonical: bool) -> NewLocation { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Other, canonical, + checksum: None, } } @@ -1104,6 +1105,7 @@ fn patch_location(location: &Location) -> PatchLocation { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: location.canonical, + checksum: location.checksum.clone(), } } diff --git a/thoth-api/src/model/file/crud.rs b/thoth-api/src/model/file/crud.rs index 251b20b10..2c4fb52a1 100644 --- a/thoth-api/src/model/file/crud.rs +++ b/thoth-api/src/model/file/crud.rs @@ -724,6 +724,7 @@ impl FileUpload { ctx: &C, work: &Work, cdn_url: &str, + cdn_checksum: &str, featured_video_dimensions: Option<(i32, i32)>, ) -> ThothResult<()> { match self.file_type { @@ -741,6 +742,7 @@ impl FileUpload { publication_id, work.landing_page.clone(), cdn_url, + Some(cdn_checksum.to_string()), )?; } FileType::AdditionalResource => { @@ -792,6 +794,7 @@ impl FileUpload { publication_id: Uuid, landing_page: Option, full_text_url: &str, + checksum: Option, ) -> ThothResult<()> { use crate::schema::location::dsl; @@ -809,6 +812,7 @@ impl FileUpload { patch.full_text_url = Some(full_text_url.to_string()); patch.landing_page = landing_page; patch.canonical = true; + patch.checksum = checksum; if patch.canonical { patch.canonical_record_complete(ctx.db())?; } @@ -830,6 +834,7 @@ impl FileUpload { full_text_url: Some(full_text_url.to_string()), location_platform: LocationPlatform::Thoth, canonical: false, + checksum, }; let created_location = Location::create(ctx.db(), &new_location)?; let mut patch = PatchLocation::from(created_location.clone()); @@ -845,6 +850,7 @@ impl FileUpload { full_text_url: Some(full_text_url.to_string()), location_platform: LocationPlatform::Thoth, canonical: true, + checksum, }; new_location.canonical_record_complete(ctx.db())?; Location::create(ctx.db(), &new_location)?; diff --git a/thoth-api/src/model/file/tests.rs b/thoth-api/src/model/file/tests.rs index 04844c8dd..70d249466 100644 --- a/thoth-api/src/model/file/tests.rs +++ b/thoth-api/src/model/file/tests.rs @@ -1243,7 +1243,7 @@ mod crud { let cover_url = "https://cdn.example.org/10.1234/abc/def_frontcover.jpg"; upload - .sync_related_metadata(&ctx, &work, cover_url, None) + .sync_related_metadata(&ctx, &work, cover_url, "checksum", None) .expect("Failed to sync frontcover metadata"); let refreshed_work = Work::from_id(pool.as_ref(), &work.work_id) @@ -1284,7 +1284,7 @@ mod crud { let video_url = "https://cdn.example.org/10.1234/abc/def/resources/video.mp4"; upload - .sync_related_metadata(&ctx, &work, video_url, Some((1280, 720))) + .sync_related_metadata(&ctx, &work, video_url, "checksum", Some((1280, 720))) .expect("Failed to sync featured-video metadata"); let refreshed = crate::model::work_featured_video::WorkFeaturedVideo::from_id( diff --git a/thoth-api/src/model/location/crud.rs b/thoth-api/src/model/location/crud.rs index a91e6723c..bded10c93 100644 --- a/thoth-api/src/model/location/crud.rs +++ b/thoth-api/src/model/location/crud.rs @@ -64,6 +64,9 @@ impl Crud for Location { LocationField::Canonical => { apply_directional_order!(query, order.direction, order, canonical) } + LocationField::Checksum => { + apply_directional_order!(query, order.direction, order, checksum) + } LocationField::CreatedAt => { apply_directional_order!(query, order.direction, order, created_at) } diff --git a/thoth-api/src/model/location/mod.rs b/thoth-api/src/model/location/mod.rs index 8dff11521..93333dc78 100644 --- a/thoth-api/src/model/location/mod.rs +++ b/thoth-api/src/model/location/mod.rs @@ -165,6 +165,7 @@ pub enum LocationField { FullTextUrl, LocationPlatform, Canonical, + Checksum, CreatedAt, UpdatedAt, } @@ -179,6 +180,7 @@ pub struct Location { pub full_text_url: Option, pub location_platform: LocationPlatform, pub canonical: bool, + pub checksum: Option, pub created_at: Timestamp, pub updated_at: Timestamp, } @@ -195,6 +197,7 @@ pub struct NewLocation { pub full_text_url: Option, pub location_platform: LocationPlatform, pub canonical: bool, + pub checksum: Option, } #[cfg_attr( @@ -210,6 +213,7 @@ pub struct PatchLocation { pub full_text_url: Option, pub location_platform: LocationPlatform, pub canonical: bool, + pub checksum: Option, } #[cfg_attr(feature = "backend", derive(diesel::Queryable))] @@ -260,6 +264,7 @@ impl From for PatchLocation { full_text_url: location.full_text_url, location_platform: location.location_platform, canonical: location.canonical, + checksum: location.checksum, } } } diff --git a/thoth-api/src/model/location/policy.rs b/thoth-api/src/model/location/policy.rs index 151799a65..dabf47d61 100644 --- a/thoth-api/src/model/location/policy.rs +++ b/thoth-api/src/model/location/policy.rs @@ -39,6 +39,11 @@ impl CreatePolicy for LocationPolicy { return Err(ThothError::ThothLocationError); } + // Only superusers can add a checksum. + if !user.is_superuser() && data.checksum.is_some() { + return Err(ThothError::CreateLocationChecksumError); + } + // Canonical locations must be complete; non-canonical locations must satisfy rules. if data.canonical { data.canonical_record_complete(ctx.db())?; @@ -74,6 +79,17 @@ impl UpdatePolicy for LocationPolicy { return Err(ThothError::ThothUpdateCanonicalError); } + // Only superusers can add a checksum. + if current.checksum.is_none() && patch.checksum.is_some() && !user.is_superuser() { + return Err(ThothError::UpdateLocationChecksumError); + } + + // Only superusers can update or delete an existing checksum. + if current.checksum.is_some() && current.checksum != patch.checksum && !user.is_superuser() + { + return Err(ThothError::UpdateLocationChecksumError); + } + // If setting canonical to true, require record completeness. if patch.canonical { patch.canonical_record_complete(ctx.db())?; diff --git a/thoth-api/src/model/location/tests.rs b/thoth-api/src/model/location/tests.rs index c87a1ee4d..9df18af8d 100644 --- a/thoth-api/src/model/location/tests.rs +++ b/thoth-api/src/model/location/tests.rs @@ -142,6 +142,7 @@ mod conversions { created_at: Default::default(), updated_at: Default::default(), canonical: true, + checksum: Some("examplechecksum".to_string()), }; let patch_location = PatchLocation::from(location.clone()); @@ -152,6 +153,7 @@ mod conversions { assert_eq!(patch_location.full_text_url, location.full_text_url); assert_eq!(patch_location.location_platform, location.location_platform); assert_eq!(patch_location.canonical, location.canonical); + assert_eq!(patch_location.checksum, location.checksum); } #[cfg(feature = "backend")] @@ -232,6 +234,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, + checksum: None, }; let location = Location::create(pool.as_ref(), &new_location).expect("Failed to create"); @@ -242,6 +245,7 @@ mod policy { full_text_url: None, location_platform: location.location_platform, canonical: location.canonical, + checksum: location.checksum.clone(), }; assert!(LocationPolicy::can_create(&ctx, &new_location, ()).is_ok()); @@ -293,6 +297,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, + checksum: None, }, ) .expect("Failed to create location"); @@ -304,6 +309,7 @@ mod policy { full_text_url: None, location_platform: location.location_platform, canonical: true, + checksum: location.checksum.clone(), }; let result = LocationPolicy::can_update(&ctx, &location, &patch, ()); @@ -334,6 +340,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, + checksum: None, }, ) .expect("Failed to create location"); @@ -345,6 +352,7 @@ mod policy { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: false, + checksum: location.checksum.clone(), }; assert!(LocationPolicy::can_update(&ctx, &location, &patch, ()).is_ok()); @@ -374,6 +382,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, + checksum: None, }, ) .expect("Failed to create canonical location"); @@ -384,6 +393,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, + checksum: None, }; assert!(LocationPolicy::can_create(&ctx, &new_location, ()).is_ok()); @@ -411,6 +421,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, + checksum: None, }; let result = LocationPolicy::can_create(&ctx, &new_location, ()); @@ -438,6 +449,7 @@ mod policy { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Thoth, canonical: true, + checksum: None, }; assert!(LocationPolicy::can_create(&ctx, &new_location, ()).is_err()); @@ -470,6 +482,7 @@ mod policy { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Thoth, canonical: true, + checksum: None, }, ) .expect("Failed to create location"); @@ -481,6 +494,7 @@ mod policy { full_text_url: Some("https://example.com/full.pdf".to_string()), location_platform: location.location_platform, canonical: location.canonical, + checksum: location.checksum.clone(), }; let update_result = LocationPolicy::can_update(&ctx, &location, &patch, ()); @@ -514,6 +528,7 @@ mod policy { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Thoth, canonical: true, + checksum: None, }, ) .expect("Failed to create canonical thoth location"); @@ -526,6 +541,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, + checksum: None, }, ) .expect("Failed to create location"); @@ -537,11 +553,98 @@ mod policy { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: true, + checksum: location.checksum.clone(), }; let result = LocationPolicy::can_update(&ctx, &location, &patch, ()); assert!(matches!(result, Err(ThothError::ThothUpdateCanonicalError))); } + + #[test] + fn crud_policy_rejects_non_superuser_checksum_create() { + let (_guard, pool) = setup_test_db(); + + let publisher = create_publisher(pool.as_ref()); + let org_id = publisher + .zitadel_id + .clone() + .expect("publisher missing zitadel id"); + let user = test_user_with_role("location-user", Role::PublisherUser, &org_id); + let ctx = test_context_with_user(pool.clone(), user); + + let imprint = create_imprint(pool.as_ref(), &publisher); + let work = create_work(pool.as_ref(), &imprint); + let publication = create_publication(pool.as_ref(), &work); + + let new_location = NewLocation { + publication_id: publication.publication_id, + landing_page: Some("https://example.com/landing".to_string()), + full_text_url: Some("https://example.com/full".to_string()), + location_platform: LocationPlatform::Other, + canonical: false, + checksum: Some("examplechecksum".to_string()), + }; + + let result = LocationPolicy::can_create(&ctx, &new_location, ()); + assert!(matches!( + result, + Err(ThothError::CreateLocationChecksumError) + )); + + let superuser = test_superuser("location-superuser"); + let super_ctx = test_context_with_user(pool.clone(), superuser); + assert!(LocationPolicy::can_create(&super_ctx, &new_location, ()).is_ok()); + } + + #[test] + fn crud_policy_rejects_non_superuser_checksum_update() { + let (_guard, pool) = setup_test_db(); + + let publisher = create_publisher(pool.as_ref()); + let org_id = publisher + .zitadel_id + .clone() + .expect("publisher missing zitadel id"); + let user = test_user_with_role("location-user", Role::PublisherUser, &org_id); + let ctx = test_context_with_user(pool.clone(), user); + + let imprint = create_imprint(pool.as_ref(), &publisher); + let work = create_work(pool.as_ref(), &imprint); + let publication = create_publication(pool.as_ref(), &work); + + let location = Location::create( + pool.as_ref(), + &NewLocation { + publication_id: publication.publication_id, + landing_page: Some("https://example.com/landing".to_string()), + full_text_url: Some("https://example.com/full".to_string()), + location_platform: LocationPlatform::Other, + canonical: false, + checksum: Some("examplechecksum".to_string()), + }, + ) + .expect("Failed to create location"); + + let patch = PatchLocation { + location_id: location.location_id, + publication_id: location.publication_id, + landing_page: location.landing_page.clone(), + full_text_url: location.full_text_url.clone(), + location_platform: location.location_platform, + canonical: location.canonical, + checksum: Some("updatedchecksum".to_string()), + }; + + let result = LocationPolicy::can_update(&ctx, &location, &patch, ()); + assert!(matches!( + result, + Err(ThothError::UpdateLocationChecksumError) + )); + + let superuser = test_superuser("location-superuser"); + let super_ctx = test_context_with_user(pool.clone(), superuser); + assert!(LocationPolicy::can_update(&super_ctx, &location, &patch, ()).is_ok()); + } } #[cfg(feature = "backend")] @@ -563,6 +666,7 @@ mod crud { location_platform: LocationPlatform, canonical: bool, landing_page: Option, + checksum: Option, ) -> Location { let new_location = NewLocation { publication_id, @@ -570,6 +674,7 @@ mod crud { full_text_url: None, location_platform, canonical, + checksum, }; Location::create(pool, &new_location).expect("Failed to create location") @@ -590,6 +695,7 @@ mod crud { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, + checksum: None, }; let location = Location::create(pool.as_ref(), &new_location).expect("Failed to create"); @@ -604,6 +710,7 @@ mod crud { full_text_url: Some("https://example.com/full.pdf".to_string()), location_platform: LocationPlatform::Other, canonical: true, + checksum: location.checksum.clone(), }; let ctx = test_context(pool.clone(), "test-user"); @@ -629,6 +736,7 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some("https://example.com/landing".to_string()), + None, ); let patch = PatchLocation { location_id: location.location_id, @@ -637,6 +745,7 @@ mod crud { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: false, + checksum: location.checksum.clone(), }; let ctx = test_context(pool.clone(), "test-user"); @@ -659,6 +768,7 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some("https://example.com/canonical".to_string()), + None, ); let non_canonical = make_location( pool.as_ref(), @@ -666,6 +776,7 @@ mod crud { LocationPlatform::Other, false, Some("https://example.com/other".to_string()), + None, ); let patch = PatchLocation { @@ -675,6 +786,7 @@ mod crud { full_text_url: non_canonical.full_text_url.clone(), location_platform: non_canonical.location_platform, canonical: true, + checksum: non_canonical.checksum.clone(), }; let ctx = test_context(pool.clone(), "test-user"); @@ -709,6 +821,7 @@ mod crud { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, + checksum: None, }; let result = new_location.can_be_non_canonical(pool.as_ref()); @@ -732,6 +845,7 @@ mod crud { full_text_url: Some("https://example.com/full.pdf".to_string()), location_platform: LocationPlatform::PublisherWebsite, canonical: true, + checksum: None, }, ) .expect("Failed to create canonical location"); @@ -742,6 +856,7 @@ mod crud { full_text_url: None, location_platform: LocationPlatform::Other, canonical: false, + checksum: None, }; assert!(new_location.can_be_non_canonical(pool.as_ref()).is_ok()); @@ -783,6 +898,7 @@ mod crud { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, + checksum: None, }; let result = new_location.canonical_record_complete(pool.as_ref()); @@ -804,6 +920,7 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, ); make_location( pool.as_ref(), @@ -811,6 +928,7 @@ mod crud { LocationPlatform::Other, false, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, ); let order = LocationOrderBy { @@ -872,6 +990,7 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, ); make_location( pool.as_ref(), @@ -879,6 +998,7 @@ mod crud { LocationPlatform::Other, false, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, ); let count = Location::count(pool.as_ref(), None, vec![], vec![], vec![], None, None) @@ -901,6 +1021,7 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, ); make_location( pool.as_ref(), @@ -908,6 +1029,7 @@ mod crud { LocationPlatform::Other, false, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, ); let count = Location::count( @@ -938,6 +1060,7 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, ); make_location( pool.as_ref(), @@ -945,6 +1068,7 @@ mod crud { LocationPlatform::Other, false, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, ); let filtered = Location::all( @@ -987,6 +1111,7 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, ); make_location( pool.as_ref(), @@ -994,6 +1119,7 @@ mod crud { LocationPlatform::Other, false, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, ); let filtered = Location::all( @@ -1033,6 +1159,7 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, ); let other_publisher = create_publisher(pool.as_ref()); @@ -1045,6 +1172,7 @@ mod crud { LocationPlatform::Other, false, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, ); let filtered = Location::all( @@ -1085,6 +1213,7 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, ); let second = make_location( pool.as_ref(), @@ -1092,6 +1221,7 @@ mod crud { LocationPlatform::Other, false, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, ); let mut ids = [first.location_id, second.location_id]; ids.sort(); @@ -1153,6 +1283,7 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, ); make_location( pool.as_ref(), @@ -1160,6 +1291,7 @@ mod crud { LocationPlatform::Other, false, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, ); let fields: Vec LocationField> = vec![ diff --git a/thoth-api/src/schema.rs b/thoth-api/src/schema.rs index caa81647d..c5391743b 100644 --- a/thoth-api/src/schema.rs +++ b/thoth-api/src/schema.rs @@ -510,6 +510,7 @@ table! { full_text_url -> Nullable, location_platform -> LocationPlatform, canonical -> Bool, + checksum -> Nullable, created_at -> Timestamptz, updated_at -> Timestamptz, } diff --git a/thoth-errors/src/lib.rs b/thoth-errors/src/lib.rs index 08209329d..7afbc0ed8 100644 --- a/thoth-errors/src/lib.rs +++ b/thoth-errors/src/lib.rs @@ -162,6 +162,10 @@ pub enum ThothError { AdditionalResourceFileUploadMissingAdditionalResourceId, #[error("Work featured video file upload missing work_featured_video_id")] WorkFeaturedVideoFileUploadMissingWorkFeaturedVideoId, + #[error("Only superusers can add a Location Checksum.")] + CreateLocationChecksumError, + #[error("Only superusers can update or delete an existing Location Checksum.")] + UpdateLocationChecksumError, } impl ThothError { From 77e26f62fec04b99aca22290e74f2dcdfcc9fc8c Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:16:51 +0100 Subject: [PATCH 2/8] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce9153eca..dbca40210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + - [747](https://github.com/thoth-pub/thoth/pull/747) - Add `Location.checksum` field ## [[1.1.1]](https://github.com/thoth-pub/thoth/releases/tag/v1.1.1) - 2026-04-24 ### Security From b5905b1c0097bc5afcaf3adf29dfeb29837ad1ff Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:26:28 +0100 Subject: [PATCH 3/8] Correct failing test --- thoth-api/src/model/location/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thoth-api/src/model/location/tests.rs b/thoth-api/src/model/location/tests.rs index 9df18af8d..4f7be0502 100644 --- a/thoth-api/src/model/location/tests.rs +++ b/thoth-api/src/model/location/tests.rs @@ -581,7 +581,7 @@ mod policy { landing_page: Some("https://example.com/landing".to_string()), full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Other, - canonical: false, + canonical: true, checksum: Some("examplechecksum".to_string()), }; From 2306d970c85c94434b321691f130cebfbc2593bb Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:28:06 +0100 Subject: [PATCH 4/8] Rename location checksum field to sha256 --- CHANGELOG.md | 2 +- thoth-api/migrations/20260429_v1.2.0/down.sql | 2 +- thoth-api/migrations/20260429_v1.2.0/up.sql | 2 +- thoth-api/src/graphql/model.rs | 6 +- thoth-api/src/graphql/tests.rs | 4 +- thoth-api/src/model/file/crud.rs | 12 ++-- thoth-api/src/model/file/tests.rs | 4 +- thoth-api/src/model/location/crud.rs | 4 +- thoth-api/src/model/location/mod.rs | 10 +-- thoth-api/src/model/location/policy.rs | 7 +- thoth-api/src/model/location/tests.rs | 64 +++++++++---------- thoth-api/src/schema.rs | 2 +- thoth-errors/src/lib.rs | 4 +- 13 files changed, 61 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbca40210..fabee53d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - - [747](https://github.com/thoth-pub/thoth/pull/747) - Add `Location.checksum` field + - [747](https://github.com/thoth-pub/thoth/pull/747) - Add `Location.sha256` field ## [[1.1.1]](https://github.com/thoth-pub/thoth/releases/tag/v1.1.1) - 2026-04-24 ### Security diff --git a/thoth-api/migrations/20260429_v1.2.0/down.sql b/thoth-api/migrations/20260429_v1.2.0/down.sql index b94303f88..6bb58b5b4 100644 --- a/thoth-api/migrations/20260429_v1.2.0/down.sql +++ b/thoth-api/migrations/20260429_v1.2.0/down.sql @@ -1,2 +1,2 @@ ALTER TABLE public.location - DROP COLUMN IF EXISTS checksum; + DROP COLUMN IF EXISTS sha256; diff --git a/thoth-api/migrations/20260429_v1.2.0/up.sql b/thoth-api/migrations/20260429_v1.2.0/up.sql index b1fc82d44..3559f8397 100644 --- a/thoth-api/migrations/20260429_v1.2.0/up.sql +++ b/thoth-api/migrations/20260429_v1.2.0/up.sql @@ -1,2 +1,2 @@ ALTER TABLE public.location - ADD COLUMN checksum TEXT; + ADD COLUMN sha256 TEXT; diff --git a/thoth-api/src/graphql/model.rs b/thoth-api/src/graphql/model.rs index 098b15c18..69b375b7a 100644 --- a/thoth-api/src/graphql/model.rs +++ b/thoth-api/src/graphql/model.rs @@ -1916,9 +1916,9 @@ impl Location { self.canonical } - #[graphql(description = "Checksum of the full text file as returned by the platform")] - pub fn checksum(&self) -> Option<&String> { - self.checksum.as_ref() + #[graphql(description = "SHA-256 checksum of the full text file as returned by the platform")] + pub fn sha256(&self) -> Option<&String> { + self.sha256.as_ref() } #[graphql(description = "Date and time at which the location record was created")] diff --git a/thoth-api/src/graphql/tests.rs b/thoth-api/src/graphql/tests.rs index f5b283c1c..bceecd514 100644 --- a/thoth-api/src/graphql/tests.rs +++ b/thoth-api/src/graphql/tests.rs @@ -470,7 +470,7 @@ fn make_new_location(publication_id: Uuid, canonical: bool) -> NewLocation { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Other, canonical, - checksum: None, + sha256: None, } } @@ -1105,7 +1105,7 @@ fn patch_location(location: &Location) -> PatchLocation { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: location.canonical, - checksum: location.checksum.clone(), + sha256: location.sha256.clone(), } } diff --git a/thoth-api/src/model/file/crud.rs b/thoth-api/src/model/file/crud.rs index 2c4fb52a1..1706c44b4 100644 --- a/thoth-api/src/model/file/crud.rs +++ b/thoth-api/src/model/file/crud.rs @@ -724,7 +724,7 @@ impl FileUpload { ctx: &C, work: &Work, cdn_url: &str, - cdn_checksum: &str, + cdn_sha256: &str, featured_video_dimensions: Option<(i32, i32)>, ) -> ThothResult<()> { match self.file_type { @@ -742,7 +742,7 @@ impl FileUpload { publication_id, work.landing_page.clone(), cdn_url, - Some(cdn_checksum.to_string()), + Some(cdn_sha256.to_string()), )?; } FileType::AdditionalResource => { @@ -794,7 +794,7 @@ impl FileUpload { publication_id: Uuid, landing_page: Option, full_text_url: &str, - checksum: Option, + sha256: Option, ) -> ThothResult<()> { use crate::schema::location::dsl; @@ -812,7 +812,7 @@ impl FileUpload { patch.full_text_url = Some(full_text_url.to_string()); patch.landing_page = landing_page; patch.canonical = true; - patch.checksum = checksum; + patch.sha256 = sha256; if patch.canonical { patch.canonical_record_complete(ctx.db())?; } @@ -834,7 +834,7 @@ impl FileUpload { full_text_url: Some(full_text_url.to_string()), location_platform: LocationPlatform::Thoth, canonical: false, - checksum, + sha256, }; let created_location = Location::create(ctx.db(), &new_location)?; let mut patch = PatchLocation::from(created_location.clone()); @@ -850,7 +850,7 @@ impl FileUpload { full_text_url: Some(full_text_url.to_string()), location_platform: LocationPlatform::Thoth, canonical: true, - checksum, + sha256, }; new_location.canonical_record_complete(ctx.db())?; Location::create(ctx.db(), &new_location)?; diff --git a/thoth-api/src/model/file/tests.rs b/thoth-api/src/model/file/tests.rs index 70d249466..682f45e81 100644 --- a/thoth-api/src/model/file/tests.rs +++ b/thoth-api/src/model/file/tests.rs @@ -1243,7 +1243,7 @@ mod crud { let cover_url = "https://cdn.example.org/10.1234/abc/def_frontcover.jpg"; upload - .sync_related_metadata(&ctx, &work, cover_url, "checksum", None) + .sync_related_metadata(&ctx, &work, cover_url, "sha256", None) .expect("Failed to sync frontcover metadata"); let refreshed_work = Work::from_id(pool.as_ref(), &work.work_id) @@ -1284,7 +1284,7 @@ mod crud { let video_url = "https://cdn.example.org/10.1234/abc/def/resources/video.mp4"; upload - .sync_related_metadata(&ctx, &work, video_url, "checksum", Some((1280, 720))) + .sync_related_metadata(&ctx, &work, video_url, "sha256", Some((1280, 720))) .expect("Failed to sync featured-video metadata"); let refreshed = crate::model::work_featured_video::WorkFeaturedVideo::from_id( diff --git a/thoth-api/src/model/location/crud.rs b/thoth-api/src/model/location/crud.rs index bded10c93..b48b5c774 100644 --- a/thoth-api/src/model/location/crud.rs +++ b/thoth-api/src/model/location/crud.rs @@ -64,8 +64,8 @@ impl Crud for Location { LocationField::Canonical => { apply_directional_order!(query, order.direction, order, canonical) } - LocationField::Checksum => { - apply_directional_order!(query, order.direction, order, checksum) + LocationField::Sha256 => { + apply_directional_order!(query, order.direction, order, sha256) } LocationField::CreatedAt => { apply_directional_order!(query, order.direction, order, created_at) diff --git a/thoth-api/src/model/location/mod.rs b/thoth-api/src/model/location/mod.rs index 93333dc78..45c8b20cd 100644 --- a/thoth-api/src/model/location/mod.rs +++ b/thoth-api/src/model/location/mod.rs @@ -165,7 +165,7 @@ pub enum LocationField { FullTextUrl, LocationPlatform, Canonical, - Checksum, + Sha256, CreatedAt, UpdatedAt, } @@ -180,7 +180,7 @@ pub struct Location { pub full_text_url: Option, pub location_platform: LocationPlatform, pub canonical: bool, - pub checksum: Option, + pub sha256: Option, pub created_at: Timestamp, pub updated_at: Timestamp, } @@ -197,7 +197,7 @@ pub struct NewLocation { pub full_text_url: Option, pub location_platform: LocationPlatform, pub canonical: bool, - pub checksum: Option, + pub sha256: Option, } #[cfg_attr( @@ -213,7 +213,7 @@ pub struct PatchLocation { pub full_text_url: Option, pub location_platform: LocationPlatform, pub canonical: bool, - pub checksum: Option, + pub sha256: Option, } #[cfg_attr(feature = "backend", derive(diesel::Queryable))] @@ -264,7 +264,7 @@ impl From for PatchLocation { full_text_url: location.full_text_url, location_platform: location.location_platform, canonical: location.canonical, - checksum: location.checksum, + sha256: location.sha256, } } } diff --git a/thoth-api/src/model/location/policy.rs b/thoth-api/src/model/location/policy.rs index dabf47d61..238551c74 100644 --- a/thoth-api/src/model/location/policy.rs +++ b/thoth-api/src/model/location/policy.rs @@ -40,7 +40,7 @@ impl CreatePolicy for LocationPolicy { } // Only superusers can add a checksum. - if !user.is_superuser() && data.checksum.is_some() { + if !user.is_superuser() && data.sha256.is_some() { return Err(ThothError::CreateLocationChecksumError); } @@ -80,13 +80,12 @@ impl UpdatePolicy for LocationPolicy { } // Only superusers can add a checksum. - if current.checksum.is_none() && patch.checksum.is_some() && !user.is_superuser() { + if current.sha256.is_none() && patch.sha256.is_some() && !user.is_superuser() { return Err(ThothError::UpdateLocationChecksumError); } // Only superusers can update or delete an existing checksum. - if current.checksum.is_some() && current.checksum != patch.checksum && !user.is_superuser() - { + if current.sha256.is_some() && current.sha256 != patch.sha256 && !user.is_superuser() { return Err(ThothError::UpdateLocationChecksumError); } diff --git a/thoth-api/src/model/location/tests.rs b/thoth-api/src/model/location/tests.rs index 4f7be0502..fe2a2368b 100644 --- a/thoth-api/src/model/location/tests.rs +++ b/thoth-api/src/model/location/tests.rs @@ -142,7 +142,7 @@ mod conversions { created_at: Default::default(), updated_at: Default::default(), canonical: true, - checksum: Some("examplechecksum".to_string()), + sha256: Some("examplesha256".to_string()), }; let patch_location = PatchLocation::from(location.clone()); @@ -153,7 +153,7 @@ mod conversions { assert_eq!(patch_location.full_text_url, location.full_text_url); assert_eq!(patch_location.location_platform, location.location_platform); assert_eq!(patch_location.canonical, location.canonical); - assert_eq!(patch_location.checksum, location.checksum); + assert_eq!(patch_location.sha256, location.sha256); } #[cfg(feature = "backend")] @@ -234,7 +234,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, - checksum: None, + sha256: None, }; let location = Location::create(pool.as_ref(), &new_location).expect("Failed to create"); @@ -245,7 +245,7 @@ mod policy { full_text_url: None, location_platform: location.location_platform, canonical: location.canonical, - checksum: location.checksum.clone(), + sha256: location.sha256.clone(), }; assert!(LocationPolicy::can_create(&ctx, &new_location, ()).is_ok()); @@ -297,7 +297,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, - checksum: None, + sha256: None, }, ) .expect("Failed to create location"); @@ -309,7 +309,7 @@ mod policy { full_text_url: None, location_platform: location.location_platform, canonical: true, - checksum: location.checksum.clone(), + sha256: location.sha256.clone(), }; let result = LocationPolicy::can_update(&ctx, &location, &patch, ()); @@ -340,7 +340,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, - checksum: None, + sha256: None, }, ) .expect("Failed to create location"); @@ -352,7 +352,7 @@ mod policy { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: false, - checksum: location.checksum.clone(), + sha256: location.sha256.clone(), }; assert!(LocationPolicy::can_update(&ctx, &location, &patch, ()).is_ok()); @@ -382,7 +382,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, - checksum: None, + sha256: None, }, ) .expect("Failed to create canonical location"); @@ -393,7 +393,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, - checksum: None, + sha256: None, }; assert!(LocationPolicy::can_create(&ctx, &new_location, ()).is_ok()); @@ -421,7 +421,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, - checksum: None, + sha256: None, }; let result = LocationPolicy::can_create(&ctx, &new_location, ()); @@ -449,7 +449,7 @@ mod policy { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Thoth, canonical: true, - checksum: None, + sha256: None, }; assert!(LocationPolicy::can_create(&ctx, &new_location, ()).is_err()); @@ -482,7 +482,7 @@ mod policy { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Thoth, canonical: true, - checksum: None, + sha256: None, }, ) .expect("Failed to create location"); @@ -494,7 +494,7 @@ mod policy { full_text_url: Some("https://example.com/full.pdf".to_string()), location_platform: location.location_platform, canonical: location.canonical, - checksum: location.checksum.clone(), + sha256: location.sha256.clone(), }; let update_result = LocationPolicy::can_update(&ctx, &location, &patch, ()); @@ -528,7 +528,7 @@ mod policy { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Thoth, canonical: true, - checksum: None, + sha256: None, }, ) .expect("Failed to create canonical thoth location"); @@ -541,7 +541,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, - checksum: None, + sha256: None, }, ) .expect("Failed to create location"); @@ -553,7 +553,7 @@ mod policy { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: true, - checksum: location.checksum.clone(), + sha256: location.sha256.clone(), }; let result = LocationPolicy::can_update(&ctx, &location, &patch, ()); @@ -561,7 +561,7 @@ mod policy { } #[test] - fn crud_policy_rejects_non_superuser_checksum_create() { + fn crud_policy_rejects_non_superuser_sha256_create() { let (_guard, pool) = setup_test_db(); let publisher = create_publisher(pool.as_ref()); @@ -582,7 +582,7 @@ mod policy { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Other, canonical: true, - checksum: Some("examplechecksum".to_string()), + sha256: Some("examplesha256".to_string()), }; let result = LocationPolicy::can_create(&ctx, &new_location, ()); @@ -597,7 +597,7 @@ mod policy { } #[test] - fn crud_policy_rejects_non_superuser_checksum_update() { + fn crud_policy_rejects_non_superuser_sha256_update() { let (_guard, pool) = setup_test_db(); let publisher = create_publisher(pool.as_ref()); @@ -620,7 +620,7 @@ mod policy { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Other, canonical: false, - checksum: Some("examplechecksum".to_string()), + sha256: Some("examplesha256".to_string()), }, ) .expect("Failed to create location"); @@ -632,7 +632,7 @@ mod policy { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: location.canonical, - checksum: Some("updatedchecksum".to_string()), + sha256: Some("updatedsha256".to_string()), }; let result = LocationPolicy::can_update(&ctx, &location, &patch, ()); @@ -666,7 +666,7 @@ mod crud { location_platform: LocationPlatform, canonical: bool, landing_page: Option, - checksum: Option, + sha256: Option, ) -> Location { let new_location = NewLocation { publication_id, @@ -674,7 +674,7 @@ mod crud { full_text_url: None, location_platform, canonical, - checksum, + sha256, }; Location::create(pool, &new_location).expect("Failed to create location") @@ -695,7 +695,7 @@ mod crud { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, - checksum: None, + sha256: None, }; let location = Location::create(pool.as_ref(), &new_location).expect("Failed to create"); @@ -710,7 +710,7 @@ mod crud { full_text_url: Some("https://example.com/full.pdf".to_string()), location_platform: LocationPlatform::Other, canonical: true, - checksum: location.checksum.clone(), + sha256: location.sha256.clone(), }; let ctx = test_context(pool.clone(), "test-user"); @@ -745,7 +745,7 @@ mod crud { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: false, - checksum: location.checksum.clone(), + sha256: location.sha256.clone(), }; let ctx = test_context(pool.clone(), "test-user"); @@ -786,7 +786,7 @@ mod crud { full_text_url: non_canonical.full_text_url.clone(), location_platform: non_canonical.location_platform, canonical: true, - checksum: non_canonical.checksum.clone(), + sha256: non_canonical.sha256.clone(), }; let ctx = test_context(pool.clone(), "test-user"); @@ -821,7 +821,7 @@ mod crud { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, - checksum: None, + sha256: None, }; let result = new_location.can_be_non_canonical(pool.as_ref()); @@ -845,7 +845,7 @@ mod crud { full_text_url: Some("https://example.com/full.pdf".to_string()), location_platform: LocationPlatform::PublisherWebsite, canonical: true, - checksum: None, + sha256: None, }, ) .expect("Failed to create canonical location"); @@ -856,7 +856,7 @@ mod crud { full_text_url: None, location_platform: LocationPlatform::Other, canonical: false, - checksum: None, + sha256: None, }; assert!(new_location.can_be_non_canonical(pool.as_ref()).is_ok()); @@ -898,7 +898,7 @@ mod crud { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, - checksum: None, + sha256: None, }; let result = new_location.canonical_record_complete(pool.as_ref()); diff --git a/thoth-api/src/schema.rs b/thoth-api/src/schema.rs index c5391743b..66e79558a 100644 --- a/thoth-api/src/schema.rs +++ b/thoth-api/src/schema.rs @@ -510,7 +510,7 @@ table! { full_text_url -> Nullable, location_platform -> LocationPlatform, canonical -> Bool, - checksum -> Nullable, + sha256 -> Nullable, created_at -> Timestamptz, updated_at -> Timestamptz, } diff --git a/thoth-errors/src/lib.rs b/thoth-errors/src/lib.rs index 7afbc0ed8..c48181a8b 100644 --- a/thoth-errors/src/lib.rs +++ b/thoth-errors/src/lib.rs @@ -162,9 +162,9 @@ pub enum ThothError { AdditionalResourceFileUploadMissingAdditionalResourceId, #[error("Work featured video file upload missing work_featured_video_id")] WorkFeaturedVideoFileUploadMissingWorkFeaturedVideoId, - #[error("Only superusers can add a Location Checksum.")] + #[error("Only superusers can add a Location SHA-256 Checksum.")] CreateLocationChecksumError, - #[error("Only superusers can update or delete an existing Location Checksum.")] + #[error("Only superusers can update or delete an existing Location SHA-256 Checksum.")] UpdateLocationChecksumError, } From c7b67a8802f2ede52190d63ecfbd176d1966a0ba Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:16:18 +0100 Subject: [PATCH 5/8] Revert "Rename location checksum field to sha256" This reverts commit 2306d970c85c94434b321691f130cebfbc2593bb. --- CHANGELOG.md | 2 +- thoth-api/migrations/20260429_v1.2.0/down.sql | 2 +- thoth-api/migrations/20260429_v1.2.0/up.sql | 2 +- thoth-api/src/graphql/model.rs | 6 +- thoth-api/src/graphql/tests.rs | 4 +- thoth-api/src/model/file/crud.rs | 12 ++-- thoth-api/src/model/file/tests.rs | 4 +- thoth-api/src/model/location/crud.rs | 4 +- thoth-api/src/model/location/mod.rs | 10 +-- thoth-api/src/model/location/policy.rs | 7 +- thoth-api/src/model/location/tests.rs | 64 +++++++++---------- thoth-api/src/schema.rs | 2 +- thoth-errors/src/lib.rs | 4 +- 13 files changed, 62 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fabee53d5..dbca40210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - - [747](https://github.com/thoth-pub/thoth/pull/747) - Add `Location.sha256` field + - [747](https://github.com/thoth-pub/thoth/pull/747) - Add `Location.checksum` field ## [[1.1.1]](https://github.com/thoth-pub/thoth/releases/tag/v1.1.1) - 2026-04-24 ### Security diff --git a/thoth-api/migrations/20260429_v1.2.0/down.sql b/thoth-api/migrations/20260429_v1.2.0/down.sql index 6bb58b5b4..b94303f88 100644 --- a/thoth-api/migrations/20260429_v1.2.0/down.sql +++ b/thoth-api/migrations/20260429_v1.2.0/down.sql @@ -1,2 +1,2 @@ ALTER TABLE public.location - DROP COLUMN IF EXISTS sha256; + DROP COLUMN IF EXISTS checksum; diff --git a/thoth-api/migrations/20260429_v1.2.0/up.sql b/thoth-api/migrations/20260429_v1.2.0/up.sql index 3559f8397..b1fc82d44 100644 --- a/thoth-api/migrations/20260429_v1.2.0/up.sql +++ b/thoth-api/migrations/20260429_v1.2.0/up.sql @@ -1,2 +1,2 @@ ALTER TABLE public.location - ADD COLUMN sha256 TEXT; + ADD COLUMN checksum TEXT; diff --git a/thoth-api/src/graphql/model.rs b/thoth-api/src/graphql/model.rs index 69b375b7a..098b15c18 100644 --- a/thoth-api/src/graphql/model.rs +++ b/thoth-api/src/graphql/model.rs @@ -1916,9 +1916,9 @@ impl Location { self.canonical } - #[graphql(description = "SHA-256 checksum of the full text file as returned by the platform")] - pub fn sha256(&self) -> Option<&String> { - self.sha256.as_ref() + #[graphql(description = "Checksum of the full text file as returned by the platform")] + pub fn checksum(&self) -> Option<&String> { + self.checksum.as_ref() } #[graphql(description = "Date and time at which the location record was created")] diff --git a/thoth-api/src/graphql/tests.rs b/thoth-api/src/graphql/tests.rs index bceecd514..f5b283c1c 100644 --- a/thoth-api/src/graphql/tests.rs +++ b/thoth-api/src/graphql/tests.rs @@ -470,7 +470,7 @@ fn make_new_location(publication_id: Uuid, canonical: bool) -> NewLocation { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Other, canonical, - sha256: None, + checksum: None, } } @@ -1105,7 +1105,7 @@ fn patch_location(location: &Location) -> PatchLocation { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: location.canonical, - sha256: location.sha256.clone(), + checksum: location.checksum.clone(), } } diff --git a/thoth-api/src/model/file/crud.rs b/thoth-api/src/model/file/crud.rs index 1706c44b4..2c4fb52a1 100644 --- a/thoth-api/src/model/file/crud.rs +++ b/thoth-api/src/model/file/crud.rs @@ -724,7 +724,7 @@ impl FileUpload { ctx: &C, work: &Work, cdn_url: &str, - cdn_sha256: &str, + cdn_checksum: &str, featured_video_dimensions: Option<(i32, i32)>, ) -> ThothResult<()> { match self.file_type { @@ -742,7 +742,7 @@ impl FileUpload { publication_id, work.landing_page.clone(), cdn_url, - Some(cdn_sha256.to_string()), + Some(cdn_checksum.to_string()), )?; } FileType::AdditionalResource => { @@ -794,7 +794,7 @@ impl FileUpload { publication_id: Uuid, landing_page: Option, full_text_url: &str, - sha256: Option, + checksum: Option, ) -> ThothResult<()> { use crate::schema::location::dsl; @@ -812,7 +812,7 @@ impl FileUpload { patch.full_text_url = Some(full_text_url.to_string()); patch.landing_page = landing_page; patch.canonical = true; - patch.sha256 = sha256; + patch.checksum = checksum; if patch.canonical { patch.canonical_record_complete(ctx.db())?; } @@ -834,7 +834,7 @@ impl FileUpload { full_text_url: Some(full_text_url.to_string()), location_platform: LocationPlatform::Thoth, canonical: false, - sha256, + checksum, }; let created_location = Location::create(ctx.db(), &new_location)?; let mut patch = PatchLocation::from(created_location.clone()); @@ -850,7 +850,7 @@ impl FileUpload { full_text_url: Some(full_text_url.to_string()), location_platform: LocationPlatform::Thoth, canonical: true, - sha256, + checksum, }; new_location.canonical_record_complete(ctx.db())?; Location::create(ctx.db(), &new_location)?; diff --git a/thoth-api/src/model/file/tests.rs b/thoth-api/src/model/file/tests.rs index 682f45e81..70d249466 100644 --- a/thoth-api/src/model/file/tests.rs +++ b/thoth-api/src/model/file/tests.rs @@ -1243,7 +1243,7 @@ mod crud { let cover_url = "https://cdn.example.org/10.1234/abc/def_frontcover.jpg"; upload - .sync_related_metadata(&ctx, &work, cover_url, "sha256", None) + .sync_related_metadata(&ctx, &work, cover_url, "checksum", None) .expect("Failed to sync frontcover metadata"); let refreshed_work = Work::from_id(pool.as_ref(), &work.work_id) @@ -1284,7 +1284,7 @@ mod crud { let video_url = "https://cdn.example.org/10.1234/abc/def/resources/video.mp4"; upload - .sync_related_metadata(&ctx, &work, video_url, "sha256", Some((1280, 720))) + .sync_related_metadata(&ctx, &work, video_url, "checksum", Some((1280, 720))) .expect("Failed to sync featured-video metadata"); let refreshed = crate::model::work_featured_video::WorkFeaturedVideo::from_id( diff --git a/thoth-api/src/model/location/crud.rs b/thoth-api/src/model/location/crud.rs index b48b5c774..bded10c93 100644 --- a/thoth-api/src/model/location/crud.rs +++ b/thoth-api/src/model/location/crud.rs @@ -64,8 +64,8 @@ impl Crud for Location { LocationField::Canonical => { apply_directional_order!(query, order.direction, order, canonical) } - LocationField::Sha256 => { - apply_directional_order!(query, order.direction, order, sha256) + LocationField::Checksum => { + apply_directional_order!(query, order.direction, order, checksum) } LocationField::CreatedAt => { apply_directional_order!(query, order.direction, order, created_at) diff --git a/thoth-api/src/model/location/mod.rs b/thoth-api/src/model/location/mod.rs index 45c8b20cd..93333dc78 100644 --- a/thoth-api/src/model/location/mod.rs +++ b/thoth-api/src/model/location/mod.rs @@ -165,7 +165,7 @@ pub enum LocationField { FullTextUrl, LocationPlatform, Canonical, - Sha256, + Checksum, CreatedAt, UpdatedAt, } @@ -180,7 +180,7 @@ pub struct Location { pub full_text_url: Option, pub location_platform: LocationPlatform, pub canonical: bool, - pub sha256: Option, + pub checksum: Option, pub created_at: Timestamp, pub updated_at: Timestamp, } @@ -197,7 +197,7 @@ pub struct NewLocation { pub full_text_url: Option, pub location_platform: LocationPlatform, pub canonical: bool, - pub sha256: Option, + pub checksum: Option, } #[cfg_attr( @@ -213,7 +213,7 @@ pub struct PatchLocation { pub full_text_url: Option, pub location_platform: LocationPlatform, pub canonical: bool, - pub sha256: Option, + pub checksum: Option, } #[cfg_attr(feature = "backend", derive(diesel::Queryable))] @@ -264,7 +264,7 @@ impl From for PatchLocation { full_text_url: location.full_text_url, location_platform: location.location_platform, canonical: location.canonical, - sha256: location.sha256, + checksum: location.checksum, } } } diff --git a/thoth-api/src/model/location/policy.rs b/thoth-api/src/model/location/policy.rs index 238551c74..dabf47d61 100644 --- a/thoth-api/src/model/location/policy.rs +++ b/thoth-api/src/model/location/policy.rs @@ -40,7 +40,7 @@ impl CreatePolicy for LocationPolicy { } // Only superusers can add a checksum. - if !user.is_superuser() && data.sha256.is_some() { + if !user.is_superuser() && data.checksum.is_some() { return Err(ThothError::CreateLocationChecksumError); } @@ -80,12 +80,13 @@ impl UpdatePolicy for LocationPolicy { } // Only superusers can add a checksum. - if current.sha256.is_none() && patch.sha256.is_some() && !user.is_superuser() { + if current.checksum.is_none() && patch.checksum.is_some() && !user.is_superuser() { return Err(ThothError::UpdateLocationChecksumError); } // Only superusers can update or delete an existing checksum. - if current.sha256.is_some() && current.sha256 != patch.sha256 && !user.is_superuser() { + if current.checksum.is_some() && current.checksum != patch.checksum && !user.is_superuser() + { return Err(ThothError::UpdateLocationChecksumError); } diff --git a/thoth-api/src/model/location/tests.rs b/thoth-api/src/model/location/tests.rs index fe2a2368b..4f7be0502 100644 --- a/thoth-api/src/model/location/tests.rs +++ b/thoth-api/src/model/location/tests.rs @@ -142,7 +142,7 @@ mod conversions { created_at: Default::default(), updated_at: Default::default(), canonical: true, - sha256: Some("examplesha256".to_string()), + checksum: Some("examplechecksum".to_string()), }; let patch_location = PatchLocation::from(location.clone()); @@ -153,7 +153,7 @@ mod conversions { assert_eq!(patch_location.full_text_url, location.full_text_url); assert_eq!(patch_location.location_platform, location.location_platform); assert_eq!(patch_location.canonical, location.canonical); - assert_eq!(patch_location.sha256, location.sha256); + assert_eq!(patch_location.checksum, location.checksum); } #[cfg(feature = "backend")] @@ -234,7 +234,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, - sha256: None, + checksum: None, }; let location = Location::create(pool.as_ref(), &new_location).expect("Failed to create"); @@ -245,7 +245,7 @@ mod policy { full_text_url: None, location_platform: location.location_platform, canonical: location.canonical, - sha256: location.sha256.clone(), + checksum: location.checksum.clone(), }; assert!(LocationPolicy::can_create(&ctx, &new_location, ()).is_ok()); @@ -297,7 +297,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, - sha256: None, + checksum: None, }, ) .expect("Failed to create location"); @@ -309,7 +309,7 @@ mod policy { full_text_url: None, location_platform: location.location_platform, canonical: true, - sha256: location.sha256.clone(), + checksum: location.checksum.clone(), }; let result = LocationPolicy::can_update(&ctx, &location, &patch, ()); @@ -340,7 +340,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, - sha256: None, + checksum: None, }, ) .expect("Failed to create location"); @@ -352,7 +352,7 @@ mod policy { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: false, - sha256: location.sha256.clone(), + checksum: location.checksum.clone(), }; assert!(LocationPolicy::can_update(&ctx, &location, &patch, ()).is_ok()); @@ -382,7 +382,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, - sha256: None, + checksum: None, }, ) .expect("Failed to create canonical location"); @@ -393,7 +393,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, - sha256: None, + checksum: None, }; assert!(LocationPolicy::can_create(&ctx, &new_location, ()).is_ok()); @@ -421,7 +421,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, - sha256: None, + checksum: None, }; let result = LocationPolicy::can_create(&ctx, &new_location, ()); @@ -449,7 +449,7 @@ mod policy { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Thoth, canonical: true, - sha256: None, + checksum: None, }; assert!(LocationPolicy::can_create(&ctx, &new_location, ()).is_err()); @@ -482,7 +482,7 @@ mod policy { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Thoth, canonical: true, - sha256: None, + checksum: None, }, ) .expect("Failed to create location"); @@ -494,7 +494,7 @@ mod policy { full_text_url: Some("https://example.com/full.pdf".to_string()), location_platform: location.location_platform, canonical: location.canonical, - sha256: location.sha256.clone(), + checksum: location.checksum.clone(), }; let update_result = LocationPolicy::can_update(&ctx, &location, &patch, ()); @@ -528,7 +528,7 @@ mod policy { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Thoth, canonical: true, - sha256: None, + checksum: None, }, ) .expect("Failed to create canonical thoth location"); @@ -541,7 +541,7 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, - sha256: None, + checksum: None, }, ) .expect("Failed to create location"); @@ -553,7 +553,7 @@ mod policy { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: true, - sha256: location.sha256.clone(), + checksum: location.checksum.clone(), }; let result = LocationPolicy::can_update(&ctx, &location, &patch, ()); @@ -561,7 +561,7 @@ mod policy { } #[test] - fn crud_policy_rejects_non_superuser_sha256_create() { + fn crud_policy_rejects_non_superuser_checksum_create() { let (_guard, pool) = setup_test_db(); let publisher = create_publisher(pool.as_ref()); @@ -582,7 +582,7 @@ mod policy { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Other, canonical: true, - sha256: Some("examplesha256".to_string()), + checksum: Some("examplechecksum".to_string()), }; let result = LocationPolicy::can_create(&ctx, &new_location, ()); @@ -597,7 +597,7 @@ mod policy { } #[test] - fn crud_policy_rejects_non_superuser_sha256_update() { + fn crud_policy_rejects_non_superuser_checksum_update() { let (_guard, pool) = setup_test_db(); let publisher = create_publisher(pool.as_ref()); @@ -620,7 +620,7 @@ mod policy { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Other, canonical: false, - sha256: Some("examplesha256".to_string()), + checksum: Some("examplechecksum".to_string()), }, ) .expect("Failed to create location"); @@ -632,7 +632,7 @@ mod policy { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: location.canonical, - sha256: Some("updatedsha256".to_string()), + checksum: Some("updatedchecksum".to_string()), }; let result = LocationPolicy::can_update(&ctx, &location, &patch, ()); @@ -666,7 +666,7 @@ mod crud { location_platform: LocationPlatform, canonical: bool, landing_page: Option, - sha256: Option, + checksum: Option, ) -> Location { let new_location = NewLocation { publication_id, @@ -674,7 +674,7 @@ mod crud { full_text_url: None, location_platform, canonical, - sha256, + checksum, }; Location::create(pool, &new_location).expect("Failed to create location") @@ -695,7 +695,7 @@ mod crud { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, - sha256: None, + checksum: None, }; let location = Location::create(pool.as_ref(), &new_location).expect("Failed to create"); @@ -710,7 +710,7 @@ mod crud { full_text_url: Some("https://example.com/full.pdf".to_string()), location_platform: LocationPlatform::Other, canonical: true, - sha256: location.sha256.clone(), + checksum: location.checksum.clone(), }; let ctx = test_context(pool.clone(), "test-user"); @@ -745,7 +745,7 @@ mod crud { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: false, - sha256: location.sha256.clone(), + checksum: location.checksum.clone(), }; let ctx = test_context(pool.clone(), "test-user"); @@ -786,7 +786,7 @@ mod crud { full_text_url: non_canonical.full_text_url.clone(), location_platform: non_canonical.location_platform, canonical: true, - sha256: non_canonical.sha256.clone(), + checksum: non_canonical.checksum.clone(), }; let ctx = test_context(pool.clone(), "test-user"); @@ -821,7 +821,7 @@ mod crud { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, - sha256: None, + checksum: None, }; let result = new_location.can_be_non_canonical(pool.as_ref()); @@ -845,7 +845,7 @@ mod crud { full_text_url: Some("https://example.com/full.pdf".to_string()), location_platform: LocationPlatform::PublisherWebsite, canonical: true, - sha256: None, + checksum: None, }, ) .expect("Failed to create canonical location"); @@ -856,7 +856,7 @@ mod crud { full_text_url: None, location_platform: LocationPlatform::Other, canonical: false, - sha256: None, + checksum: None, }; assert!(new_location.can_be_non_canonical(pool.as_ref()).is_ok()); @@ -898,7 +898,7 @@ mod crud { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, - sha256: None, + checksum: None, }; let result = new_location.canonical_record_complete(pool.as_ref()); diff --git a/thoth-api/src/schema.rs b/thoth-api/src/schema.rs index 66e79558a..c5391743b 100644 --- a/thoth-api/src/schema.rs +++ b/thoth-api/src/schema.rs @@ -510,7 +510,7 @@ table! { full_text_url -> Nullable, location_platform -> LocationPlatform, canonical -> Bool, - sha256 -> Nullable, + checksum -> Nullable, created_at -> Timestamptz, updated_at -> Timestamptz, } diff --git a/thoth-errors/src/lib.rs b/thoth-errors/src/lib.rs index c48181a8b..7afbc0ed8 100644 --- a/thoth-errors/src/lib.rs +++ b/thoth-errors/src/lib.rs @@ -162,9 +162,9 @@ pub enum ThothError { AdditionalResourceFileUploadMissingAdditionalResourceId, #[error("Work featured video file upload missing work_featured_video_id")] WorkFeaturedVideoFileUploadMissingWorkFeaturedVideoId, - #[error("Only superusers can add a Location SHA-256 Checksum.")] + #[error("Only superusers can add a Location Checksum.")] CreateLocationChecksumError, - #[error("Only superusers can update or delete an existing Location SHA-256 Checksum.")] + #[error("Only superusers can update or delete an existing Location Checksum.")] UpdateLocationChecksumError, } From 6f383df4010bf4533e0fb8285758cd02219c0372 Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:18:30 +0100 Subject: [PATCH 6/8] Add checksum_algorithm field to specify details of location checksum --- CHANGELOG.md | 2 +- thoth-api/migrations/20260429_v1.2.0/down.sql | 6 ++- thoth-api/migrations/20260429_v1.2.0/up.sql | 9 +++- thoth-api/src/graphql/model.rs | 7 ++- thoth-api/src/graphql/tests.rs | 2 + thoth-api/src/model/file/crud.rs | 20 ++++---- thoth-api/src/model/file/mod.rs | 16 +++++++ thoth-api/src/model/location/crud.rs | 3 ++ thoth-api/src/model/location/mod.rs | 7 ++- thoth-api/src/model/location/tests.rs | 48 +++++++++++++++++++ thoth-api/src/schema.rs | 6 +++ thoth-errors/src/database_errors.rs | 1 + 12 files changed, 113 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbca40210..d2ec107cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - - [747](https://github.com/thoth-pub/thoth/pull/747) - Add `Location.checksum` field + - [747](https://github.com/thoth-pub/thoth/pull/747) - Add `checksum` and `checksum_algorithm` fields to `Location` ## [[1.1.1]](https://github.com/thoth-pub/thoth/releases/tag/v1.1.1) - 2026-04-24 ### Security diff --git a/thoth-api/migrations/20260429_v1.2.0/down.sql b/thoth-api/migrations/20260429_v1.2.0/down.sql index b94303f88..915683617 100644 --- a/thoth-api/migrations/20260429_v1.2.0/down.sql +++ b/thoth-api/migrations/20260429_v1.2.0/down.sql @@ -1,2 +1,6 @@ ALTER TABLE public.location - DROP COLUMN IF EXISTS checksum; + DROP CONSTRAINT IF EXISTS location_checksum_and_algorithm_all_or_none, + DROP COLUMN IF EXISTS checksum, + DROP COLUMN IF EXISTS checksum_algorithm, + +DROP TYPE IF EXISTS public.checksum_algorithm; diff --git a/thoth-api/migrations/20260429_v1.2.0/up.sql b/thoth-api/migrations/20260429_v1.2.0/up.sql index b1fc82d44..ce8266eb5 100644 --- a/thoth-api/migrations/20260429_v1.2.0/up.sql +++ b/thoth-api/migrations/20260429_v1.2.0/up.sql @@ -1,2 +1,9 @@ +CREATE TYPE public.checksum_algorithm AS ENUM ( + 'MD5', + 'SHA256' +); + ALTER TABLE public.location - ADD COLUMN checksum TEXT; + ADD COLUMN checksum TEXT, + ADD COLUMN checksum_algorithm public.checksum_algorithm, + ADD CONSTRAINT location_checksum_and_algorithm_all_or_none CHECK ((checksum IS NULL AND checksum_algorithm IS NULL) OR (checksum IS NOT NULL AND checksum_algorithm IS NOT NULL)); diff --git a/thoth-api/src/graphql/model.rs b/thoth-api/src/graphql/model.rs index 098b15c18..694d724d1 100644 --- a/thoth-api/src/graphql/model.rs +++ b/thoth-api/src/graphql/model.rs @@ -21,7 +21,7 @@ use crate::model::{ contribution::{Contribution, ContributionType}, contributor::Contributor, endorsement::{Endorsement, EndorsementOrderBy}, - file::{File, FileType}, + file::{ChecksumAlgorithm, File, FileType}, funding::Funding, imprint::{Imprint, ImprintField, ImprintOrderBy}, institution::Institution, @@ -1921,6 +1921,11 @@ impl Location { self.checksum.as_ref() } + #[graphql(description = "Algorithm used to generate the checksum (MD5 or SHA-256)")] + pub fn checksum_algorithm(&self) -> Option<&ChecksumAlgorithm> { + self.checksum_algorithm.as_ref() + } + #[graphql(description = "Date and time at which the location record was created")] pub fn created_at(&self) -> Timestamp { self.created_at diff --git a/thoth-api/src/graphql/tests.rs b/thoth-api/src/graphql/tests.rs index f5b283c1c..f697da4bd 100644 --- a/thoth-api/src/graphql/tests.rs +++ b/thoth-api/src/graphql/tests.rs @@ -471,6 +471,7 @@ fn make_new_location(publication_id: Uuid, canonical: bool) -> NewLocation { location_platform: LocationPlatform::Other, canonical, checksum: None, + checksum_algorithm: None, } } @@ -1106,6 +1107,7 @@ fn patch_location(location: &Location) -> PatchLocation { location_platform: location.location_platform, canonical: location.canonical, checksum: location.checksum.clone(), + checksum_algorithm: location.checksum_algorithm, } } diff --git a/thoth-api/src/model/file/crud.rs b/thoth-api/src/model/file/crud.rs index 2c4fb52a1..1c5789a04 100644 --- a/thoth-api/src/model/file/crud.rs +++ b/thoth-api/src/model/file/crud.rs @@ -1,7 +1,6 @@ -use super::FileType; use super::{ - upload_request_headers, File, FileCleanupCandidate, FilePolicy, FileUpload, FileUploadResponse, - NewFile, NewFileUpload, + upload_request_headers, ChecksumAlgorithm, File, FileCleanupCandidate, FilePolicy, FileType, + FileUpload, FileUploadResponse, NewFile, NewFileUpload, }; use crate::db::PgPool; use crate::model::{ @@ -724,7 +723,7 @@ impl FileUpload { ctx: &C, work: &Work, cdn_url: &str, - cdn_checksum: &str, + cdn_sha256: &str, featured_video_dimensions: Option<(i32, i32)>, ) -> ThothResult<()> { match self.file_type { @@ -742,7 +741,7 @@ impl FileUpload { publication_id, work.landing_page.clone(), cdn_url, - Some(cdn_checksum.to_string()), + Some(cdn_sha256.to_string()), )?; } FileType::AdditionalResource => { @@ -794,7 +793,7 @@ impl FileUpload { publication_id: Uuid, landing_page: Option, full_text_url: &str, - checksum: Option, + sha256: Option, ) -> ThothResult<()> { use crate::schema::location::dsl; @@ -812,7 +811,8 @@ impl FileUpload { patch.full_text_url = Some(full_text_url.to_string()); patch.landing_page = landing_page; patch.canonical = true; - patch.checksum = checksum; + patch.checksum = sha256; + patch.checksum_algorithm = Some(ChecksumAlgorithm::Sha256); if patch.canonical { patch.canonical_record_complete(ctx.db())?; } @@ -834,7 +834,8 @@ impl FileUpload { full_text_url: Some(full_text_url.to_string()), location_platform: LocationPlatform::Thoth, canonical: false, - checksum, + checksum: sha256, + checksum_algorithm: Some(ChecksumAlgorithm::Sha256), }; let created_location = Location::create(ctx.db(), &new_location)?; let mut patch = PatchLocation::from(created_location.clone()); @@ -850,7 +851,8 @@ impl FileUpload { full_text_url: Some(full_text_url.to_string()), location_platform: LocationPlatform::Thoth, canonical: true, - checksum, + checksum: sha256, + checksum_algorithm: Some(ChecksumAlgorithm::Sha256), }; new_location.canonical_record_complete(ctx.db())?; Location::create(ctx.db(), &new_location)?; diff --git a/thoth-api/src/model/file/mod.rs b/thoth-api/src/model/file/mod.rs index 5e83912d7..8573fcbb8 100644 --- a/thoth-api/src/model/file/mod.rs +++ b/thoth-api/src/model/file/mod.rs @@ -50,6 +50,22 @@ pub enum FileType { WorkFeaturedVideo, } +#[cfg_attr( + feature = "backend", + derive(diesel_derive_enum::DbEnum, juniper::GraphQLEnum), + graphql(description = "Algorithm used to create file checksum"), + ExistingTypePath = "crate::schema::sql_types::ChecksumAlgorithm" +)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, EnumString, Display)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +pub enum ChecksumAlgorithm { + #[cfg_attr(feature = "backend", db_rename = "MD5")] + Md5, + #[cfg_attr(feature = "backend", db_rename = "SHA256")] + Sha256, +} + #[cfg_attr(feature = "backend", derive(diesel::Queryable))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] diff --git a/thoth-api/src/model/location/crud.rs b/thoth-api/src/model/location/crud.rs index bded10c93..44ace7661 100644 --- a/thoth-api/src/model/location/crud.rs +++ b/thoth-api/src/model/location/crud.rs @@ -67,6 +67,9 @@ impl Crud for Location { LocationField::Checksum => { apply_directional_order!(query, order.direction, order, checksum) } + LocationField::ChecksumAlgorithm => { + apply_directional_order!(query, order.direction, order, checksum_algorithm) + } LocationField::CreatedAt => { apply_directional_order!(query, order.direction, order, created_at) } diff --git a/thoth-api/src/model/location/mod.rs b/thoth-api/src/model/location/mod.rs index 93333dc78..c35a25cd9 100644 --- a/thoth-api/src/model/location/mod.rs +++ b/thoth-api/src/model/location/mod.rs @@ -4,7 +4,7 @@ use strum::EnumString; use uuid::Uuid; use crate::graphql::types::inputs::Direction; -use crate::model::Timestamp; +use crate::model::{file::ChecksumAlgorithm, Timestamp}; #[cfg(feature = "backend")] use crate::schema::location; #[cfg(feature = "backend")] @@ -166,6 +166,7 @@ pub enum LocationField { LocationPlatform, Canonical, Checksum, + ChecksumAlgorithm, CreatedAt, UpdatedAt, } @@ -181,6 +182,7 @@ pub struct Location { pub location_platform: LocationPlatform, pub canonical: bool, pub checksum: Option, + pub checksum_algorithm: Option, pub created_at: Timestamp, pub updated_at: Timestamp, } @@ -198,6 +200,7 @@ pub struct NewLocation { pub location_platform: LocationPlatform, pub canonical: bool, pub checksum: Option, + pub checksum_algorithm: Option, } #[cfg_attr( @@ -214,6 +217,7 @@ pub struct PatchLocation { pub location_platform: LocationPlatform, pub canonical: bool, pub checksum: Option, + pub checksum_algorithm: Option, } #[cfg_attr(feature = "backend", derive(diesel::Queryable))] @@ -265,6 +269,7 @@ impl From for PatchLocation { location_platform: location.location_platform, canonical: location.canonical, checksum: location.checksum, + checksum_algorithm: location.checksum_algorithm, } } } diff --git a/thoth-api/src/model/location/tests.rs b/thoth-api/src/model/location/tests.rs index 4f7be0502..bafa8c2a3 100644 --- a/thoth-api/src/model/location/tests.rs +++ b/thoth-api/src/model/location/tests.rs @@ -143,6 +143,7 @@ mod conversions { updated_at: Default::default(), canonical: true, checksum: Some("examplechecksum".to_string()), + checksum_algorithm: Some(ChecksumAlgorithm::Md5), }; let patch_location = PatchLocation::from(location.clone()); @@ -235,6 +236,7 @@ mod policy { location_platform: LocationPlatform::PublisherWebsite, canonical: true, checksum: None, + checksum_algorithm: None, }; let location = Location::create(pool.as_ref(), &new_location).expect("Failed to create"); @@ -246,6 +248,7 @@ mod policy { location_platform: location.location_platform, canonical: location.canonical, checksum: location.checksum.clone(), + checksum_algorithm: location.checksum_algorithm, }; assert!(LocationPolicy::can_create(&ctx, &new_location, ()).is_ok()); @@ -298,6 +301,7 @@ mod policy { location_platform: LocationPlatform::PublisherWebsite, canonical: false, checksum: None, + checksum_algorithm: None, }, ) .expect("Failed to create location"); @@ -310,6 +314,7 @@ mod policy { location_platform: location.location_platform, canonical: true, checksum: location.checksum.clone(), + checksum_algorithm: location.checksum_algorithm, }; let result = LocationPolicy::can_update(&ctx, &location, &patch, ()); @@ -341,6 +346,7 @@ mod policy { location_platform: LocationPlatform::PublisherWebsite, canonical: true, checksum: None, + checksum_algorithm: None, }, ) .expect("Failed to create location"); @@ -353,6 +359,7 @@ mod policy { location_platform: location.location_platform, canonical: false, checksum: location.checksum.clone(), + checksum_algorithm: location.checksum_algorithm, }; assert!(LocationPolicy::can_update(&ctx, &location, &patch, ()).is_ok()); @@ -383,6 +390,7 @@ mod policy { location_platform: LocationPlatform::PublisherWebsite, canonical: true, checksum: None, + checksum_algorithm: None, }, ) .expect("Failed to create canonical location"); @@ -394,6 +402,7 @@ mod policy { location_platform: LocationPlatform::PublisherWebsite, canonical: false, checksum: None, + checksum_algorithm: None, }; assert!(LocationPolicy::can_create(&ctx, &new_location, ()).is_ok()); @@ -422,6 +431,7 @@ mod policy { location_platform: LocationPlatform::PublisherWebsite, canonical: false, checksum: None, + checksum_algorithm: None, }; let result = LocationPolicy::can_create(&ctx, &new_location, ()); @@ -450,6 +460,7 @@ mod policy { location_platform: LocationPlatform::Thoth, canonical: true, checksum: None, + checksum_algorithm: None, }; assert!(LocationPolicy::can_create(&ctx, &new_location, ()).is_err()); @@ -483,6 +494,7 @@ mod policy { location_platform: LocationPlatform::Thoth, canonical: true, checksum: None, + checksum_algorithm: None, }, ) .expect("Failed to create location"); @@ -495,6 +507,7 @@ mod policy { location_platform: location.location_platform, canonical: location.canonical, checksum: location.checksum.clone(), + checksum_algorithm: location.checksum_algorithm, }; let update_result = LocationPolicy::can_update(&ctx, &location, &patch, ()); @@ -529,6 +542,7 @@ mod policy { location_platform: LocationPlatform::Thoth, canonical: true, checksum: None, + checksum_algorithm: None, }, ) .expect("Failed to create canonical thoth location"); @@ -542,6 +556,7 @@ mod policy { location_platform: LocationPlatform::PublisherWebsite, canonical: false, checksum: None, + checksum_algorithm: None, }, ) .expect("Failed to create location"); @@ -554,6 +569,7 @@ mod policy { location_platform: location.location_platform, canonical: true, checksum: location.checksum.clone(), + checksum_algorithm: location.checksum_algorithm, }; let result = LocationPolicy::can_update(&ctx, &location, &patch, ()); @@ -583,6 +599,7 @@ mod policy { location_platform: LocationPlatform::Other, canonical: true, checksum: Some("examplechecksum".to_string()), + checksum_algorithm: Some(ChecksumAlgorithm::Md5), }; let result = LocationPolicy::can_create(&ctx, &new_location, ()); @@ -621,6 +638,7 @@ mod policy { location_platform: LocationPlatform::Other, canonical: false, checksum: Some("examplechecksum".to_string()), + checksum_algorithm: Some(ChecksumAlgorithm::Md5), }, ) .expect("Failed to create location"); @@ -633,6 +651,7 @@ mod policy { location_platform: location.location_platform, canonical: location.canonical, checksum: Some("updatedchecksum".to_string()), + checksum_algorithm: location.checksum_algorithm, }; let result = LocationPolicy::can_update(&ctx, &location, &patch, ()); @@ -667,6 +686,7 @@ mod crud { canonical: bool, landing_page: Option, checksum: Option, + checksum_algorithm: Option, ) -> Location { let new_location = NewLocation { publication_id, @@ -675,6 +695,7 @@ mod crud { location_platform, canonical, checksum, + checksum_algorithm, }; Location::create(pool, &new_location).expect("Failed to create location") @@ -696,6 +717,7 @@ mod crud { location_platform: LocationPlatform::PublisherWebsite, canonical: true, checksum: None, + checksum_algorithm: None, }; let location = Location::create(pool.as_ref(), &new_location).expect("Failed to create"); @@ -711,6 +733,7 @@ mod crud { location_platform: LocationPlatform::Other, canonical: true, checksum: location.checksum.clone(), + checksum_algorithm: location.checksum_algorithm, }; let ctx = test_context(pool.clone(), "test-user"); @@ -737,6 +760,7 @@ mod crud { true, Some("https://example.com/landing".to_string()), None, + None, ); let patch = PatchLocation { location_id: location.location_id, @@ -746,6 +770,7 @@ mod crud { location_platform: location.location_platform, canonical: false, checksum: location.checksum.clone(), + checksum_algorithm: location.checksum_algorithm, }; let ctx = test_context(pool.clone(), "test-user"); @@ -769,6 +794,7 @@ mod crud { true, Some("https://example.com/canonical".to_string()), None, + None, ); let non_canonical = make_location( pool.as_ref(), @@ -777,6 +803,7 @@ mod crud { false, Some("https://example.com/other".to_string()), None, + None, ); let patch = PatchLocation { @@ -787,6 +814,7 @@ mod crud { location_platform: non_canonical.location_platform, canonical: true, checksum: non_canonical.checksum.clone(), + checksum_algorithm: non_canonical.checksum_algorithm, }; let ctx = test_context(pool.clone(), "test-user"); @@ -822,6 +850,7 @@ mod crud { location_platform: LocationPlatform::PublisherWebsite, canonical: false, checksum: None, + checksum_algorithm: None, }; let result = new_location.can_be_non_canonical(pool.as_ref()); @@ -846,6 +875,7 @@ mod crud { location_platform: LocationPlatform::PublisherWebsite, canonical: true, checksum: None, + checksum_algorithm: None, }, ) .expect("Failed to create canonical location"); @@ -857,6 +887,7 @@ mod crud { location_platform: LocationPlatform::Other, canonical: false, checksum: None, + checksum_algorithm: None, }; assert!(new_location.can_be_non_canonical(pool.as_ref()).is_ok()); @@ -899,6 +930,7 @@ mod crud { location_platform: LocationPlatform::PublisherWebsite, canonical: true, checksum: None, + checksum_algorithm: None, }; let result = new_location.canonical_record_complete(pool.as_ref()); @@ -921,6 +953,7 @@ mod crud { true, Some(format!("https://example.com/{}", Uuid::new_v4())), None, + None, ); make_location( pool.as_ref(), @@ -929,6 +962,7 @@ mod crud { false, Some(format!("https://example.com/{}", Uuid::new_v4())), None, + None, ); let order = LocationOrderBy { @@ -991,6 +1025,7 @@ mod crud { true, Some(format!("https://example.com/{}", Uuid::new_v4())), None, + None, ); make_location( pool.as_ref(), @@ -999,6 +1034,7 @@ mod crud { false, Some(format!("https://example.com/{}", Uuid::new_v4())), None, + None, ); let count = Location::count(pool.as_ref(), None, vec![], vec![], vec![], None, None) @@ -1022,6 +1058,7 @@ mod crud { true, Some(format!("https://example.com/{}", Uuid::new_v4())), None, + None, ); make_location( pool.as_ref(), @@ -1030,6 +1067,7 @@ mod crud { false, Some(format!("https://example.com/{}", Uuid::new_v4())), None, + None, ); let count = Location::count( @@ -1061,6 +1099,7 @@ mod crud { true, Some(format!("https://example.com/{}", Uuid::new_v4())), None, + None, ); make_location( pool.as_ref(), @@ -1069,6 +1108,7 @@ mod crud { false, Some(format!("https://example.com/{}", Uuid::new_v4())), None, + None, ); let filtered = Location::all( @@ -1112,6 +1152,7 @@ mod crud { true, Some(format!("https://example.com/{}", Uuid::new_v4())), None, + None, ); make_location( pool.as_ref(), @@ -1120,6 +1161,7 @@ mod crud { false, Some(format!("https://example.com/{}", Uuid::new_v4())), None, + None, ); let filtered = Location::all( @@ -1160,6 +1202,7 @@ mod crud { true, Some(format!("https://example.com/{}", Uuid::new_v4())), None, + None, ); let other_publisher = create_publisher(pool.as_ref()); @@ -1173,6 +1216,7 @@ mod crud { false, Some(format!("https://example.com/{}", Uuid::new_v4())), None, + None, ); let filtered = Location::all( @@ -1214,6 +1258,7 @@ mod crud { true, Some(format!("https://example.com/{}", Uuid::new_v4())), None, + None, ); let second = make_location( pool.as_ref(), @@ -1222,6 +1267,7 @@ mod crud { false, Some(format!("https://example.com/{}", Uuid::new_v4())), None, + None, ); let mut ids = [first.location_id, second.location_id]; ids.sort(); @@ -1284,6 +1330,7 @@ mod crud { true, Some(format!("https://example.com/{}", Uuid::new_v4())), None, + None, ); make_location( pool.as_ref(), @@ -1292,6 +1339,7 @@ mod crud { false, Some(format!("https://example.com/{}", Uuid::new_v4())), None, + None, ); let fields: Vec LocationField> = vec![ diff --git a/thoth-api/src/schema.rs b/thoth-api/src/schema.rs index c5391743b..160bd15af 100644 --- a/thoth-api/src/schema.rs +++ b/thoth-api/src/schema.rs @@ -82,6 +82,10 @@ pub mod sql_types { #[derive(diesel::sql_types::SqlType, diesel::query_builder::QueryId)] #[diesel(postgres_type(name = "accessibility_exception"))] pub struct AccessibilityException; + + #[derive(diesel::sql_types::SqlType, diesel::query_builder::QueryId)] + #[diesel(postgres_type(name = "checksum_algorithm"))] + pub struct ChecksumAlgorithm; } use diesel::{allow_tables_to_appear_in_same_query, joinable, table}; @@ -501,6 +505,7 @@ table! { table! { use diesel::sql_types::*; + use super::sql_types::ChecksumAlgorithm; use super::sql_types::LocationPlatform; location (location_id) { @@ -511,6 +516,7 @@ table! { location_platform -> LocationPlatform, canonical -> Bool, checksum -> Nullable, + checksum_algorithm -> Nullable, created_at -> Timestamptz, updated_at -> Timestamptz, } diff --git a/thoth-errors/src/database_errors.rs b/thoth-errors/src/database_errors.rs index 5d61ad29c..0a39835d4 100644 --- a/thoth-errors/src/database_errors.rs +++ b/thoth-errors/src/database_errors.rs @@ -65,6 +65,7 @@ static DATABASE_CONSTRAINT_ERRORS: Map<&'static str, &'static str> = phf_map! { "issue_series_id_work_id_uniq" => "An issue on the selected series already exists for this work.", "issue_issue_ordinal_series_id_uniq" => "An issue with this ordinal number already exists.", "language_uniq_work_idx" => "Duplicate language code.", + "location_checksum_and_algorithm_all_or_none" => "Location checksum and checksum_algorithm must be provided together, or both must be empty.", "location_full_text_url_check" => "Invalid URL.", "location_landing_page_check" => "Invalid URL.", "location_uniq_canonical_true_idx" => "A canonical location for this publication already exists.", From a0afe5287c83bf8eb2870c1178f5a85f0e0f8903 Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:34:22 +0100 Subject: [PATCH 7/8] Correct location checksum policy logic and fix migration typo --- thoth-api/migrations/20260429_v1.2.0/down.sql | 2 +- thoth-api/src/model/location/policy.rs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/thoth-api/migrations/20260429_v1.2.0/down.sql b/thoth-api/migrations/20260429_v1.2.0/down.sql index 915683617..296c3083c 100644 --- a/thoth-api/migrations/20260429_v1.2.0/down.sql +++ b/thoth-api/migrations/20260429_v1.2.0/down.sql @@ -1,6 +1,6 @@ ALTER TABLE public.location DROP CONSTRAINT IF EXISTS location_checksum_and_algorithm_all_or_none, DROP COLUMN IF EXISTS checksum, - DROP COLUMN IF EXISTS checksum_algorithm, + DROP COLUMN IF EXISTS checksum_algorithm; DROP TYPE IF EXISTS public.checksum_algorithm; diff --git a/thoth-api/src/model/location/policy.rs b/thoth-api/src/model/location/policy.rs index dabf47d61..a55ffecd4 100644 --- a/thoth-api/src/model/location/policy.rs +++ b/thoth-api/src/model/location/policy.rs @@ -85,7 +85,10 @@ impl UpdatePolicy for LocationPolicy { } // Only superusers can update or delete an existing checksum. - if current.checksum.is_some() && current.checksum != patch.checksum && !user.is_superuser() + if ((current.checksum.is_some() && current.checksum != patch.checksum) + || (current.checksum_algorithm.is_some() + && current.checksum_algorithm != patch.checksum_algorithm)) + && !user.is_superuser() { return Err(ThothError::UpdateLocationChecksumError); } From e5b003a6a1a5068d0d038ad802c7dfe870bb47cd Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:08:27 +0100 Subject: [PATCH 8/8] Add SHA-1 algorithm option --- thoth-api/migrations/20260429_v1.2.0/up.sql | 3 ++- thoth-api/src/graphql/model.rs | 2 +- thoth-api/src/model/file/mod.rs | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/thoth-api/migrations/20260429_v1.2.0/up.sql b/thoth-api/migrations/20260429_v1.2.0/up.sql index ce8266eb5..389adf546 100644 --- a/thoth-api/migrations/20260429_v1.2.0/up.sql +++ b/thoth-api/migrations/20260429_v1.2.0/up.sql @@ -1,6 +1,7 @@ CREATE TYPE public.checksum_algorithm AS ENUM ( 'MD5', - 'SHA256' + 'SHA256', + 'SHA1' ); ALTER TABLE public.location diff --git a/thoth-api/src/graphql/model.rs b/thoth-api/src/graphql/model.rs index 694d724d1..3c09f228f 100644 --- a/thoth-api/src/graphql/model.rs +++ b/thoth-api/src/graphql/model.rs @@ -1921,7 +1921,7 @@ impl Location { self.checksum.as_ref() } - #[graphql(description = "Algorithm used to generate the checksum (MD5 or SHA-256)")] + #[graphql(description = "Algorithm used to generate the checksum (MD5, SHA-256 or SHA-1)")] pub fn checksum_algorithm(&self) -> Option<&ChecksumAlgorithm> { self.checksum_algorithm.as_ref() } diff --git a/thoth-api/src/model/file/mod.rs b/thoth-api/src/model/file/mod.rs index 8573fcbb8..c45ce2a7d 100644 --- a/thoth-api/src/model/file/mod.rs +++ b/thoth-api/src/model/file/mod.rs @@ -64,6 +64,8 @@ pub enum ChecksumAlgorithm { Md5, #[cfg_attr(feature = "backend", db_rename = "SHA256")] Sha256, + #[cfg_attr(feature = "backend", db_rename = "SHA1")] + Sha1, } #[cfg_attr(feature = "backend", derive(diesel::Queryable))]