hyrax 5.2.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (527) hide show
  1. checksums.yaml +4 -4
  2. data/.dassie/.env +5 -1
  3. data/.dassie/Gemfile +4 -2
  4. data/.dassie/app/assets/images/bulkrax/removed.png +0 -0
  5. data/.dassie/app/assets/javascripts/application.js +2 -0
  6. data/.dassie/app/assets/stylesheets/application.css +1 -0
  7. data/.dassie/app/controllers/catalog_controller.rb +1 -0
  8. data/.dassie/app/forms/admin_set_resource_form.rb +6 -0
  9. data/.dassie/app/forms/collection_resource_form.rb +5 -2
  10. data/.dassie/app/forms/generic_work_resource_form.rb +5 -2
  11. data/.dassie/app/forms/monograph_form.rb +5 -3
  12. data/.dassie/app/indexers/admin_set_resource_indexer.rb +5 -0
  13. data/.dassie/app/indexers/collection_resource_indexer.rb +3 -3
  14. data/.dassie/app/indexers/generic_work_resource_indexer.rb +6 -3
  15. data/.dassie/app/indexers/monograph_indexer.rb +6 -2
  16. data/.dassie/app/models/ability.rb +8 -0
  17. data/.dassie/app/models/collection_resource.rb +4 -2
  18. data/.dassie/app/models/concerns/bulkrax/has_local_processing.rb +8 -0
  19. data/.dassie/app/models/generic_work_resource.rb +5 -2
  20. data/.dassie/app/models/monograph.rb +5 -2
  21. data/.dassie/app/views/blacklight_dynamic_sitemap/sitemap/show.xml.builder +22 -0
  22. data/.dassie/bin/importer +142 -0
  23. data/.dassie/bin/web +4 -0
  24. data/.dassie/config/browse_everything_providers.yml +1 -1
  25. data/.dassie/config/bulkrax_api.yml +86 -0
  26. data/.dassie/config/initializers/bulkrax.rb +92 -0
  27. data/.dassie/config/initializers/clamav.rb +2 -0
  28. data/.dassie/config/initializers/hyrax.rb +7 -1
  29. data/.dassie/config/initializers/riiif.rb +4 -80
  30. data/.dassie/config/initializers/wings.rb +1 -1
  31. data/.dassie/config/metadata_profiles/m3_profile.yaml +929 -0
  32. data/.dassie/config/puma.rb +4 -2
  33. data/.dassie/config/routes.rb +7 -0
  34. data/.dassie/config/sidekiq.yml +17 -0
  35. data/.dassie/config/uv/uv.html +63 -81
  36. data/.dassie/db/migrate/20240606205215_create_hyrax_flexible_schemas.rb +9 -0
  37. data/.dassie/db/migrate/20240606205216_add_contexts_to_hyrax_flexible_schemas.rb +5 -0
  38. data/.dassie/db/migrate/20250826142438_create_bulkrax_importers.bulkrax.rb +19 -0
  39. data/.dassie/db/migrate/20250826142439_create_bulkrax_importer_runs.bulkrax.rb +17 -0
  40. data/.dassie/db/migrate/20250826142440_create_bulkrax_entries.bulkrax.rb +17 -0
  41. data/.dassie/db/migrate/20250826142441_add_status_to_entry.bulkrax.rb +10 -0
  42. data/.dassie/db/migrate/20250826142442_add_collections_to_importer_runs.bulkrax.rb +7 -0
  43. data/.dassie/db/migrate/20250826142443_change_collection_ids_on_entries.bulkrax.rb +6 -0
  44. data/.dassie/db/migrate/20250826142444_create_bulkrax_exporters.bulkrax.rb +20 -0
  45. data/.dassie/db/migrate/20250826142445_create_bulkrax_exporter_runs.bulkrax.rb +15 -0
  46. data/.dassie/db/migrate/20250826142446_change_importer_and_exporter_to_polymorphic.bulkrax.rb +19 -0
  47. data/.dassie/db/migrate/20250826142447_add_total_collection_records_to_importer_runs.bulkrax.rb +6 -0
  48. data/.dassie/db/migrate/20250826142448_add_children_to_importer_runs.bulkrax.rb +7 -0
  49. data/.dassie/db/migrate/20250826142449_change_total_records_to_total_work_entries.bulkrax.rb +7 -0
  50. data/.dassie/db/migrate/20250826142450_change_entry_last_error.bulkrax.rb +20 -0
  51. data/.dassie/db/migrate/20250826142451_add_validate_only_to_bulkrax_importers.bulkrax.rb +6 -0
  52. data/.dassie/db/migrate/20250826142452_add_status_to_importers.bulkrax.rb +10 -0
  53. data/.dassie/db/migrate/20250826142453_remove_foreign_key_from_bulkrax_entries.bulkrax.rb +6 -0
  54. data/.dassie/db/migrate/20250826142454_add_status_to_exporters.bulkrax.rb +8 -0
  55. data/.dassie/db/migrate/20250826142455_add_invalid_record_to_importer_run.bulkrax.rb +6 -0
  56. data/.dassie/db/migrate/20250826142456_create_bulkrax_statuses.bulkrax.rb +19 -0
  57. data/.dassie/db/migrate/20250826142457_move_to_statuses.bulkrax.rb +31 -0
  58. data/.dassie/db/migrate/20250826142458_add_date_filter_and_status_to_bulkrax_exporters.bulkrax.rb +8 -0
  59. data/.dassie/db/migrate/20250826142459_add_workflow_status_to_bulkrax_exporter.bulkrax.rb +6 -0
  60. data/.dassie/db/migrate/20250826142460_remove_unused_last_error.bulkrax.rb +8 -0
  61. data/.dassie/db/migrate/20250826142461_increase_text_sizes.bulkrax.rb +13 -0
  62. data/.dassie/db/migrate/20250826142462_change_bulkrax_statuses_error_message_column_type_to_text.bulkrax.rb +6 -0
  63. data/.dassie/db/migrate/20250826142463_rename_children_counters_to_relationships.bulkrax.rb +7 -0
  64. data/.dassie/db/migrate/20250826142464_add_file_set_counters_to_importer_runs.bulkrax.rb +8 -0
  65. data/.dassie/db/migrate/20250826142465_add_import_attempts_to_entries.bulkrax.rb +6 -0
  66. data/.dassie/db/migrate/20250826142466_add_work_counters_to_importer_runs.bulkrax.rb +7 -0
  67. data/.dassie/db/migrate/20250826142467_create_bulkrax_pending_relationships.bulkrax.rb +14 -0
  68. data/.dassie/db/migrate/20250826142468_add_order_to_bulkrax_pending_relationships.bulkrax.rb +6 -0
  69. data/.dassie/db/migrate/20250826142469_add_include_thumbnails_to_bulkrax_exporters.bulkrax.rb +6 -0
  70. data/.dassie/db/migrate/20250826142470_add_generated_metadata_to_bulkrax_exporters.bulkrax.rb +6 -0
  71. data/.dassie/db/migrate/20250826142471_rename_bulkrax_importer_run_to_importer_run.bulkrax.rb +18 -0
  72. data/.dassie/db/migrate/20250826142472_add_indices_to_bulkrax.bulkrax.rb +26 -0
  73. data/.dassie/db/migrate/20250826142473_denormalize_status_message.bulkrax.rb +8 -0
  74. data/.dassie/db/migrate/20250826142474_update_identifier_index.bulkrax.rb +7 -0
  75. data/.dassie/db/migrate/20250826142475_add_index_to_metadata_bulkrax_identifier.bulkrax.rb +19 -0
  76. data/.dassie/db/migrate/20250826142476_add_file_name_to_uploaded_files.bulkrax.rb +6 -0
  77. data/.dassie/db/migrate/20250826142477_add_error_tracking_to_pending_relationships.bulkrax.rb +6 -0
  78. data/.dassie/db/migrate/20250826142478_add_last_imported_at_to_bulkrax_importers.bulkrax.rb +6 -0
  79. data/.dassie/db/migrate/20250826142479_add_next_import_at_to_bulkrax_importers.bulkrax.rb +6 -0
  80. data/.dassie/db/migrate/20250826142480_entry_error_denormalization.bulkrax.rb +8 -0
  81. data/.dassie/db/migrate/20250826142481_faster_first_entry.bulkrax.rb +8 -0
  82. data/.dassie/db/migrate/20260430000001_create_hyrax_redirect_paths.hyrax.rb +13 -0
  83. data/.dassie/db/migrate/20260521003627_add_additional_redirect_info.hyrax.rb +32 -0
  84. data/.dassie/db/schema.rb +154 -1
  85. data/.dassie/package.json +1 -1
  86. data/.dassie/rspec_split_configuration.json +835 -0
  87. data/.dassie/solr/conf/solrconfig.xml +21 -5
  88. data/.github/{ISSUE_TEMPLATE.md → ISSUE_TEMPLATE/issue_template.md} +5 -0
  89. data/.github/workflows/lint-build-test.yml +5 -1
  90. data/.github/workflows/release-charts.yml +48 -0
  91. data/.gitignore +6 -0
  92. data/.koppie/.env +5 -1
  93. data/.koppie/Gemfile +3 -1
  94. data/.koppie/app/assets/images/bulkrax/removed.png +0 -0
  95. data/.koppie/app/assets/javascripts/application.js +2 -0
  96. data/.koppie/app/assets/stylesheets/application.css +1 -0
  97. data/.koppie/app/controllers/catalog_controller.rb +1 -1
  98. data/.koppie/app/forms/collection_resource_form.rb +5 -2
  99. data/.koppie/app/forms/file_set_form.rb +5 -0
  100. data/.koppie/app/forms/generic_work_form.rb +5 -3
  101. data/.koppie/app/forms/monograph_form.rb +5 -3
  102. data/.koppie/app/indexers/collection_resource_indexer.rb +5 -2
  103. data/.koppie/app/indexers/file_set_indexer.rb +7 -0
  104. data/.koppie/app/indexers/generic_work_indexer.rb +6 -3
  105. data/.koppie/app/indexers/monograph_indexer.rb +6 -2
  106. data/.koppie/app/models/ability.rb +8 -0
  107. data/.koppie/app/models/collection_resource.rb +4 -2
  108. data/.koppie/app/models/concerns/bulkrax/has_local_processing.rb +8 -0
  109. data/.koppie/app/models/generic_work.rb +5 -2
  110. data/.koppie/app/models/monograph.rb +5 -2
  111. data/.koppie/app/views/blacklight_dynamic_sitemap/sitemap/show.xml.builder +22 -0
  112. data/.koppie/bin/importer +142 -0
  113. data/.koppie/bin/web +4 -0
  114. data/.koppie/config/browse_everything_providers.yml +2 -0
  115. data/.koppie/config/bulkrax_api.yml +86 -0
  116. data/.koppie/config/initializers/bulkrax.rb +92 -0
  117. data/.koppie/config/initializers/clamav.rb +2 -0
  118. data/.koppie/config/initializers/hyrax.rb +11 -3
  119. data/.koppie/config/initializers/riiif.rb +4 -82
  120. data/.koppie/config/metadata_profiles/m3_profile.yaml +930 -0
  121. data/.koppie/config/routes.rb +7 -0
  122. data/.koppie/config/sidekiq.yml +2 -0
  123. data/.koppie/config/uv/uv.html +63 -81
  124. data/.koppie/db/migrate/20240606205215_create_hyrax_flexible_schemas.rb +9 -0
  125. data/.koppie/db/migrate/20240606205216_add_contexts_to_hyrax_flexible_schemas.rb +5 -0
  126. data/.koppie/db/migrate/20250826142438_create_bulkrax_importers.bulkrax.rb +19 -0
  127. data/.koppie/db/migrate/20250826142439_create_bulkrax_importer_runs.bulkrax.rb +17 -0
  128. data/.koppie/db/migrate/20250826142440_create_bulkrax_entries.bulkrax.rb +17 -0
  129. data/.koppie/db/migrate/20250826142441_add_status_to_entry.bulkrax.rb +10 -0
  130. data/.koppie/db/migrate/20250826142442_add_collections_to_importer_runs.bulkrax.rb +7 -0
  131. data/.koppie/db/migrate/20250826142443_change_collection_ids_on_entries.bulkrax.rb +6 -0
  132. data/.koppie/db/migrate/20250826142444_create_bulkrax_exporters.bulkrax.rb +20 -0
  133. data/.koppie/db/migrate/20250826142445_create_bulkrax_exporter_runs.bulkrax.rb +15 -0
  134. data/.koppie/db/migrate/20250826142446_change_importer_and_exporter_to_polymorphic.bulkrax.rb +19 -0
  135. data/.koppie/db/migrate/20250826142447_add_total_collection_records_to_importer_runs.bulkrax.rb +6 -0
  136. data/.koppie/db/migrate/20250826142448_add_children_to_importer_runs.bulkrax.rb +7 -0
  137. data/.koppie/db/migrate/20250826142449_change_total_records_to_total_work_entries.bulkrax.rb +7 -0
  138. data/.koppie/db/migrate/20250826142450_change_entry_last_error.bulkrax.rb +20 -0
  139. data/.koppie/db/migrate/20250826142451_add_validate_only_to_bulkrax_importers.bulkrax.rb +6 -0
  140. data/.koppie/db/migrate/20250826142452_add_status_to_importers.bulkrax.rb +10 -0
  141. data/.koppie/db/migrate/20250826142453_remove_foreign_key_from_bulkrax_entries.bulkrax.rb +6 -0
  142. data/.koppie/db/migrate/20250826142454_add_status_to_exporters.bulkrax.rb +8 -0
  143. data/.koppie/db/migrate/20250826142455_add_invalid_record_to_importer_run.bulkrax.rb +6 -0
  144. data/.koppie/db/migrate/20250826142456_create_bulkrax_statuses.bulkrax.rb +19 -0
  145. data/.koppie/db/migrate/20250826142457_move_to_statuses.bulkrax.rb +31 -0
  146. data/.koppie/db/migrate/20250826142458_add_date_filter_and_status_to_bulkrax_exporters.bulkrax.rb +8 -0
  147. data/.koppie/db/migrate/20250826142459_add_workflow_status_to_bulkrax_exporter.bulkrax.rb +6 -0
  148. data/.koppie/db/migrate/20250826142460_remove_unused_last_error.bulkrax.rb +8 -0
  149. data/.koppie/db/migrate/20250826142461_increase_text_sizes.bulkrax.rb +13 -0
  150. data/.koppie/db/migrate/20250826142462_change_bulkrax_statuses_error_message_column_type_to_text.bulkrax.rb +6 -0
  151. data/.koppie/db/migrate/20250826142463_rename_children_counters_to_relationships.bulkrax.rb +7 -0
  152. data/.koppie/db/migrate/20250826142464_add_file_set_counters_to_importer_runs.bulkrax.rb +8 -0
  153. data/.koppie/db/migrate/20250826142465_add_import_attempts_to_entries.bulkrax.rb +6 -0
  154. data/.koppie/db/migrate/20250826142466_add_work_counters_to_importer_runs.bulkrax.rb +7 -0
  155. data/.koppie/db/migrate/20250826142467_create_bulkrax_pending_relationships.bulkrax.rb +14 -0
  156. data/.koppie/db/migrate/20250826142468_add_order_to_bulkrax_pending_relationships.bulkrax.rb +6 -0
  157. data/.koppie/db/migrate/20250826142469_add_include_thumbnails_to_bulkrax_exporters.bulkrax.rb +6 -0
  158. data/.koppie/db/migrate/20250826142470_add_generated_metadata_to_bulkrax_exporters.bulkrax.rb +6 -0
  159. data/.koppie/db/migrate/20250826142471_rename_bulkrax_importer_run_to_importer_run.bulkrax.rb +18 -0
  160. data/.koppie/db/migrate/20250826142472_add_indices_to_bulkrax.bulkrax.rb +26 -0
  161. data/.koppie/db/migrate/20250826142473_denormalize_status_message.bulkrax.rb +8 -0
  162. data/.koppie/db/migrate/20250826142474_update_identifier_index.bulkrax.rb +7 -0
  163. data/.koppie/db/migrate/20250826142475_add_index_to_metadata_bulkrax_identifier.bulkrax.rb +19 -0
  164. data/.koppie/db/migrate/20250826142476_add_file_name_to_uploaded_files.bulkrax.rb +6 -0
  165. data/.koppie/db/migrate/20250826142477_add_error_tracking_to_pending_relationships.bulkrax.rb +6 -0
  166. data/.koppie/db/migrate/20250826142478_add_last_imported_at_to_bulkrax_importers.bulkrax.rb +6 -0
  167. data/.koppie/db/migrate/20250826142479_add_next_import_at_to_bulkrax_importers.bulkrax.rb +6 -0
  168. data/.koppie/db/migrate/20250826142480_entry_error_denormalization.bulkrax.rb +8 -0
  169. data/.koppie/db/migrate/20250826142481_faster_first_entry.bulkrax.rb +8 -0
  170. data/.koppie/db/migrate/20260430000001_create_hyrax_redirect_paths.hyrax.rb +13 -0
  171. data/.koppie/db/migrate/20260521003627_add_additional_redirect_info.hyrax.rb +32 -0
  172. data/.koppie/db/schema.rb +154 -1
  173. data/.koppie/package.json +1 -1
  174. data/.koppie/rspec_split_configuration.json +835 -0
  175. data/.koppie/solr/conf/solrconfig.xml +0 -5
  176. data/.koppie/yarn.lock +5587 -752
  177. data/.rubocop.yml +1 -0
  178. data/CONTAINERS.md +2 -0
  179. data/Dockerfile +9 -22
  180. data/Gemfile +0 -1
  181. data/Gemfile.allinson +2 -0
  182. data/README.md +1 -2
  183. data/app/actors/hyrax/actors/file_actor.rb +2 -0
  184. data/app/actors/hyrax/actors/file_set_actor.rb +1 -1
  185. data/app/assets/javascripts/hyrax/app.js.erb +143 -10
  186. data/app/assets/javascripts/hyrax/collection_select.js +68 -0
  187. data/app/assets/javascripts/hyrax/copy_permalink_button.js +28 -0
  188. data/app/assets/javascripts/hyrax/redirects.js +45 -0
  189. data/app/assets/javascripts/hyrax/relationships/control.es6 +21 -1
  190. data/app/assets/javascripts/hyrax/select_work_type.es6 +7 -2
  191. data/app/assets/javascripts/hyrax.js +3 -1
  192. data/app/assets/stylesheets/hyrax/_file-show.scss +1 -0
  193. data/app/assets/stylesheets/hyrax/_viewer.scss +8 -0
  194. data/app/assets/stylesheets/hyrax/dashboard.scss +22 -1
  195. data/app/assets/stylesheets/hyrax/sidebar.scss +98 -3
  196. data/app/authorities/qa/authorities/collections.rb +9 -9
  197. data/app/controllers/concerns/hyrax/analytics_error_handling.rb +97 -0
  198. data/app/controllers/concerns/hyrax/collections_controller_behavior.rb +3 -1
  199. data/app/controllers/concerns/hyrax/ensure_migrated_behavior.rb +50 -0
  200. data/app/controllers/concerns/hyrax/flexible_catalog_behavior.rb +206 -0
  201. data/app/controllers/concerns/hyrax/flexible_schema_behavior.rb +17 -0
  202. data/app/controllers/concerns/hyrax/redirect_to_display_url.rb +54 -0
  203. data/app/controllers/concerns/hyrax/works_controller_behavior.rb +52 -10
  204. data/app/controllers/hyrax/admin/admin_sets_controller.rb +21 -13
  205. data/app/controllers/hyrax/admin/analytics/collection_reports_controller.rb +2 -0
  206. data/app/controllers/hyrax/admin/analytics/work_reports_controller.rb +1 -0
  207. data/app/controllers/hyrax/admin/metadata_profiles_controller.rb +66 -0
  208. data/app/controllers/hyrax/batch_uploads_controller.rb +2 -0
  209. data/app/controllers/hyrax/dashboard/collections_controller.rb +27 -6
  210. data/app/controllers/hyrax/dashboard/nest_collections_controller.rb +2 -1
  211. data/app/controllers/hyrax/dashboard_controller.rb +2 -1
  212. data/app/controllers/hyrax/file_sets_controller.rb +19 -1
  213. data/app/controllers/hyrax/homepage_controller.rb +7 -0
  214. data/app/controllers/hyrax/my/works_controller.rb +2 -21
  215. data/app/controllers/hyrax/permissions_controller.rb +4 -2
  216. data/app/controllers/hyrax/redirects_controller.rb +56 -0
  217. data/app/controllers/hyrax/single_use_links_viewer_controller.rb +2 -1
  218. data/app/controllers/hyrax/transcripts_controller.rb +31 -0
  219. data/app/controllers/hyrax/uploads_controller.rb +18 -9
  220. data/app/forms/concerns/hyrax/based_near_field_behavior.rb +77 -0
  221. data/app/forms/concerns/hyrax/flexible_form_behavior.rb +37 -0
  222. data/app/forms/concerns/hyrax/redirects_field_behavior.rb +95 -0
  223. data/app/forms/concerns/hyrax/transcripts_behavior.rb +40 -0
  224. data/app/forms/hyrax/forms/admin_set_form.rb +1 -1
  225. data/app/forms/hyrax/forms/administrative_set_form.rb +3 -0
  226. data/app/forms/hyrax/forms/batch_edit_form.rb +1 -3
  227. data/app/forms/hyrax/forms/batch_upload_form.rb +14 -0
  228. data/app/forms/hyrax/forms/file_set_edit_form.rb +1 -1
  229. data/app/forms/hyrax/forms/file_set_form.rb +6 -7
  230. data/app/forms/hyrax/forms/pcdm_collection_form.rb +17 -5
  231. data/app/forms/hyrax/forms/pcdm_object_form.rb +11 -3
  232. data/app/forms/hyrax/forms/resource_batch_edit_form.rb +8 -22
  233. data/app/forms/hyrax/forms/resource_form.rb +110 -10
  234. data/app/forms/hyrax/forms.rb +1 -0
  235. data/app/helpers/hyrax/attributes_helper.rb +109 -0
  236. data/app/helpers/hyrax/blacklight_override.rb +26 -0
  237. data/app/helpers/hyrax/collections_helper.rb +2 -0
  238. data/app/helpers/hyrax/file_set_form_helper.rb +24 -0
  239. data/app/helpers/hyrax/hyrax_helper_behavior.rb +51 -2
  240. data/app/helpers/hyrax/iiif_helper.rb +11 -1
  241. data/app/helpers/hyrax/permalink_helper.rb +37 -0
  242. data/app/helpers/hyrax/redirects_tab_helper.rb +29 -0
  243. data/app/helpers/hyrax/work_form_helper.rb +9 -5
  244. data/app/helpers/hyrax/works_helper.rb +14 -0
  245. data/app/indexers/concerns/hyrax/workflow_indexer.rb +30 -0
  246. data/app/indexers/hyrax/file_set_indexer.rb +1 -0
  247. data/app/indexers/hyrax/indexers/administrative_set_indexer.rb +2 -1
  248. data/app/indexers/hyrax/indexers/file_set_indexer.rb +6 -2
  249. data/app/indexers/hyrax/indexers/pcdm_collection_indexer.rb +11 -1
  250. data/app/indexers/hyrax/indexers/pcdm_object_indexer.rb +25 -4
  251. data/app/indexers/hyrax/indexers/redirects_indexer.rb +29 -0
  252. data/app/indexers/hyrax/indexers/resource_indexer.rb +4 -0
  253. data/app/indexers/hyrax/indexers.rb +2 -0
  254. data/app/inputs/controlled_vocabulary_input.rb +7 -1
  255. data/app/jobs/content_deposit_error_event_job.rb +14 -0
  256. data/app/jobs/create_work_job.rb +2 -0
  257. data/app/jobs/migrate_files_to_valkyrie_job.rb +7 -1
  258. data/app/models/batch_upload_item.rb +4 -0
  259. data/app/models/concerns/hyrax/ability/flexible_metadata_ability.rb +10 -0
  260. data/app/models/concerns/hyrax/ability.rb +3 -1
  261. data/app/models/concerns/hyrax/collection_behavior.rb +4 -0
  262. data/app/models/concerns/hyrax/file_set/transcripts.rb +16 -0
  263. data/app/models/concerns/hyrax/file_set_behavior.rb +9 -0
  264. data/app/models/concerns/hyrax/flexibility.rb +107 -0
  265. data/app/models/concerns/hyrax/redirects_normalization.rb +38 -0
  266. data/app/models/concerns/hyrax/solr_document/characterization.rb +1 -1
  267. data/app/models/concerns/hyrax/solr_document/metadata.rb +9 -0
  268. data/app/models/concerns/hyrax/solr_document/ordered_members.rb +4 -0
  269. data/app/models/concerns/hyrax/solr_document_behavior.rb +28 -6
  270. data/app/models/concerns/hyrax/suppressible.rb +1 -1
  271. data/app/models/concerns/hyrax/valkyrie_lazy_migration.rb +1 -1
  272. data/app/models/concerns/hyrax/with_embargoes_and_leases.rb +30 -0
  273. data/app/models/concerns/hyrax/work_behavior.rb +8 -0
  274. data/app/models/hyrax/administrative_set.rb +1 -1
  275. data/app/models/hyrax/change_set.rb +3 -1
  276. data/app/models/hyrax/embargo.rb +3 -1
  277. data/app/models/hyrax/file_set.rb +4 -2
  278. data/app/models/hyrax/flexible_schema.rb +219 -0
  279. data/app/models/hyrax/lease.rb +3 -1
  280. data/app/models/hyrax/pcdm_collection.rb +4 -1
  281. data/app/models/hyrax/redirect.rb +64 -0
  282. data/app/models/hyrax/redirect_path.rb +14 -0
  283. data/app/models/hyrax/resource.rb +22 -25
  284. data/app/models/hyrax/riiif/file.rb +43 -0
  285. data/app/models/hyrax/riiif/file_resolver.rb +65 -0
  286. data/app/models/hyrax/uploaded_file.rb +9 -0
  287. data/app/models/hyrax/virus_scanner.rb +16 -3
  288. data/app/models/hyrax/work.rb +4 -1
  289. data/app/models/job_io_wrapper.rb +12 -3
  290. data/app/models/user_mailbox.rb +3 -1
  291. data/app/presenters/hyrax/admin_set_presenter.rb +11 -0
  292. data/app/presenters/hyrax/annotates_content.rb +35 -0
  293. data/app/presenters/hyrax/collection_presenter.rb +5 -1
  294. data/app/presenters/hyrax/displays_content.rb +133 -0
  295. data/app/presenters/hyrax/displays_image.rb +11 -4
  296. data/app/presenters/hyrax/displays_transcripts.rb +59 -0
  297. data/app/presenters/hyrax/file_set_presenter.rb +8 -18
  298. data/app/presenters/hyrax/google_scholar_presenter.rb +39 -0
  299. data/app/presenters/hyrax/iiif_manifest_presenter.rb +22 -4
  300. data/app/presenters/hyrax/missing_method_behavior.rb +15 -0
  301. data/app/presenters/hyrax/pcdm_member_presenter_factory.rb +2 -4
  302. data/app/presenters/hyrax/work_show_presenter.rb +56 -14
  303. data/app/renderers/hyrax/renderers/attribute_renderer.rb +6 -2
  304. data/app/renderers/hyrax/renderers/license_attribute_renderer.rb +10 -10
  305. data/app/renderers/hyrax/renderers/redirects_label_attribute_renderer.rb +25 -0
  306. data/app/renderers/hyrax/renderers/rights_statement_attribute_renderer.rb +10 -11
  307. data/app/search_builders/hyrax/dashboard/nested_collections_search_builder.rb +1 -5
  308. data/app/search_builders/hyrax/filter_by_type.rb +1 -1
  309. data/app/services/hyrax/action/create_valkyrie_work.rb +3 -2
  310. data/app/services/hyrax/authority_rendering_helper.rb +28 -0
  311. data/app/services/hyrax/authority_service.rb +107 -0
  312. data/app/services/hyrax/callbacks.rb +2 -2
  313. data/app/services/hyrax/collections/nested_collection_query_service.rb +9 -3
  314. data/app/services/hyrax/collections_service.rb +17 -0
  315. data/app/services/hyrax/custom_queries/find_by_property_value.rb +89 -0
  316. data/app/services/hyrax/edit_permissions_service.rb +4 -4
  317. data/app/services/hyrax/file_set_type_service.rb +12 -4
  318. data/app/services/hyrax/flexible_schema_validator_service.rb +153 -0
  319. data/app/services/hyrax/flexible_schema_validators/class_validator.rb +123 -0
  320. data/app/services/hyrax/flexible_schema_validators/core_metadata_validator.rb +211 -0
  321. data/app/services/hyrax/flexible_schema_validators/existing_records_validator.rb +83 -0
  322. data/app/services/hyrax/flexible_schema_validators/redirects_validator.rb +116 -0
  323. data/app/services/hyrax/flexible_schema_validators/schema_validator.rb +34 -0
  324. data/app/services/hyrax/flexible_schema_validators/sort_properties_validator.rb +54 -0
  325. data/app/services/hyrax/license_service.rb +1 -1
  326. data/app/services/hyrax/listeners/metadata_index_listener.rb +11 -0
  327. data/app/services/hyrax/m3_profile_editor.rb +32 -0
  328. data/app/services/hyrax/m3_schema_loader.rb +119 -0
  329. data/app/services/hyrax/manifest_builder_service.rb +18 -24
  330. data/app/services/hyrax/permalink_path.rb +25 -0
  331. data/app/services/hyrax/qa_select_service.rb +9 -5
  332. data/app/services/hyrax/redirect_path_normalizer.rb +58 -0
  333. data/app/services/hyrax/redirects_lookup.rb +59 -0
  334. data/app/services/hyrax/resource_permissions_visibility_propagator.rb +54 -0
  335. data/app/services/hyrax/resource_types_service.rb +3 -20
  336. data/app/services/hyrax/rights_statement_service.rb +1 -1
  337. data/app/services/hyrax/schema_loader.rb +205 -0
  338. data/app/services/hyrax/search_service.rb +1 -1
  339. data/app/services/hyrax/simple_schema_loader.rb +28 -127
  340. data/app/services/hyrax/solr_service.rb +39 -2
  341. data/app/services/hyrax/tolerant_select_service.rb +5 -3
  342. data/app/services/hyrax/valkyrie_upload.rb +2 -2
  343. data/app/services/hyrax/visibility_propagator.rb +4 -1
  344. data/app/services/hyrax/work_uploads_handler.rb +17 -9
  345. data/app/services/hyrax/workflow/workflow_factory.rb +5 -4
  346. data/app/services/migrate_resource_service.rb +7 -1
  347. data/app/uploaders/hyrax/uploaded_file_uploader.rb +2 -2
  348. data/app/utils/hyrax/data_destroyers/collection_branding_destroyer.rb +1 -1
  349. data/app/utils/hyrax/required_data_seeder.rb +1 -0
  350. data/app/utils/hyrax/required_data_seeders/collection_type_seeder.rb +1 -1
  351. data/app/utils/hyrax/required_data_seeders/flexible_profile_seeder.rb +28 -0
  352. data/app/validators/hyrax/controlled_vocabulary_validator.rb +64 -0
  353. data/app/validators/hyrax/redirect_validator.rb +128 -0
  354. data/app/views/catalog/_thumbnail_list_default.html.erb +2 -2
  355. data/app/views/collections/edit_fields/_schema_version.html.erb +1 -0
  356. data/app/views/hyrax/admin/admin_sets/_form_metadata.html.erb +24 -2
  357. data/app/views/hyrax/admin/admin_sets/_show_document_list_row.html.erb +1 -1
  358. data/app/views/hyrax/admin/admin_sets/show.html.erb +1 -1
  359. data/app/views/hyrax/admin/analytics/_analytics_error.html.erb +42 -0
  360. data/app/views/hyrax/admin/analytics/collection_reports/index.html.erb +11 -9
  361. data/app/views/hyrax/admin/analytics/work_reports/index.html.erb +60 -57
  362. data/app/views/hyrax/admin/features/index.html.erb +1 -1
  363. data/app/views/hyrax/admin/metadata_profiles/_import_modal.html.erb +20 -0
  364. data/app/views/hyrax/admin/metadata_profiles/index.html.erb +54 -0
  365. data/app/views/hyrax/base/_attribute_rows.html.erb +4 -21
  366. data/app/views/hyrax/base/_citations.html.erb +1 -0
  367. data/app/views/hyrax/base/_currently_shared.html.erb +2 -1
  368. data/app/views/hyrax/base/_file_manager_thumbnail.html.erb +1 -1
  369. data/app/views/hyrax/base/_form_member_of_collections.html.erb +6 -6
  370. data/app/views/hyrax/base/_form_redirects.html.erb +125 -0
  371. data/app/views/hyrax/base/_form_share.html.erb +7 -2
  372. data/app/views/hyrax/base/_member.html.erb +1 -1
  373. data/app/views/hyrax/base/_social_media.html.erb +1 -1
  374. data/app/views/hyrax/base/iiif_viewers/_universal_viewer.html.erb +1 -1
  375. data/app/views/hyrax/base/show.html.erb +3 -0
  376. data/app/views/hyrax/collections/_default_group.html.erb +3 -0
  377. data/app/views/hyrax/collections/_list_collections.html.erb +3 -0
  378. data/app/views/hyrax/collections/_list_works.html.erb +1 -1
  379. data/app/views/hyrax/collections/_show_document_list_row.html.erb +1 -1
  380. data/app/views/hyrax/collections/show.html.erb +4 -0
  381. data/app/views/hyrax/dashboard/_sidebar.html.erb +4 -2
  382. data/app/views/hyrax/dashboard/collections/_button_for_update_collection.html.erb +2 -1
  383. data/app/views/hyrax/dashboard/collections/_default_group.html.erb +3 -0
  384. data/app/views/hyrax/dashboard/collections/_form.html.erb +17 -0
  385. data/app/views/hyrax/dashboard/collections/_form_for_select_collection.html.erb +7 -10
  386. data/app/views/hyrax/dashboard/collections/_list_collections.html.erb +3 -0
  387. data/app/views/hyrax/dashboard/collections/_list_works.html.erb +1 -1
  388. data/app/views/hyrax/dashboard/collections/_show_document_list_row.html.erb +1 -1
  389. data/app/views/hyrax/dashboard/profiles/_edit_primary.html.erb +1 -8
  390. data/app/views/hyrax/dashboard/profiles/_trophy_edit.html.erb +1 -1
  391. data/app/views/hyrax/dashboard/profiles/show.html.erb +5 -5
  392. data/app/views/hyrax/dashboard/sidebar/_metadata.html.erb +11 -0
  393. data/app/views/hyrax/dashboard/sidebar/_repository_content.html.erb +1 -0
  394. data/app/views/hyrax/dashboard/works/_default_group.html.erb +3 -0
  395. data/app/views/hyrax/dashboard/works/_list_works.html.erb +4 -2
  396. data/app/views/hyrax/file_sets/_form.html.erb +16 -3
  397. data/app/views/hyrax/file_sets/_metadata.html.erb +35 -0
  398. data/app/views/hyrax/file_sets/_valkyrie_form.html.erb +37 -0
  399. data/app/views/hyrax/file_sets/edit.html.erb +14 -8
  400. data/app/views/hyrax/file_sets/media_display/_audio.html.erb +4 -0
  401. data/app/views/hyrax/file_sets/media_display/_image.html.erb +2 -2
  402. data/app/views/hyrax/file_sets/media_display/_video.html.erb +4 -0
  403. data/app/views/hyrax/homepage/_explore_collections.html.erb +1 -1
  404. data/app/views/hyrax/homepage/_featured_fields.html.erb +1 -1
  405. data/app/views/hyrax/homepage/_recent_document.html.erb +1 -1
  406. data/app/views/hyrax/homepage/robots.text.erb +17 -0
  407. data/app/views/hyrax/my/collections/_default_group.html.erb +3 -0
  408. data/app/views/hyrax/my/collections/_list_collections.html.erb +3 -0
  409. data/app/views/hyrax/my/collections/_modal_add_subcollection.html.erb +2 -2
  410. data/app/views/hyrax/my/collections/_modal_add_to_collection.html.erb +2 -2
  411. data/app/views/hyrax/my/works/_default_group.html.erb +3 -0
  412. data/app/views/hyrax/my/works/_list_works.html.erb +5 -2
  413. data/app/views/hyrax/shared/_copy_permalink.html.erb +10 -0
  414. data/app/views/hyrax/users/_user_info.html.erb +1 -6
  415. data/app/views/layouts/_head_tag_content.html.erb +2 -0
  416. data/app/views/layouts/hyrax/dashboard.html.erb +2 -1
  417. data/app/views/records/edit_fields/_contexts.html.erb +1 -0
  418. data/app/views/records/edit_fields/_license.html.erb +17 -5
  419. data/app/views/records/edit_fields/_resource_type.html.erb +16 -2
  420. data/app/views/records/edit_fields/_rights_statement.html.erb +17 -5
  421. data/app/views/records/edit_fields/_schema_version.html.erb +1 -0
  422. data/app/views/records/edit_fields/_transcript_ids.html.erb +18 -0
  423. data/app/views/records/show_fields/_rights.html.erb +1 -1
  424. data/app/views/shared/_citations.html.erb +46 -31
  425. data/app/views/shared/_schema_version.html.erb +38 -0
  426. data/bin/dev-entrypoint.sh +14 -4
  427. data/chart/hyrax/Chart.yaml +4 -4
  428. data/chart/hyrax/templates/ingress.yaml +3 -0
  429. data/chart/hyrax/values.yaml +10 -8
  430. data/config/features.rb +27 -0
  431. data/config/initializers/blacklight_dynamic_sitemap.rb +43 -0
  432. data/config/initializers/deprecation_patch.rb +27 -0
  433. data/config/initializers/hydra_works.rb +3 -0
  434. data/config/initializers/i18n_extensions.rb +197 -0
  435. data/config/initializers/storage_adapter_initializer.rb +15 -9
  436. data/config/locales/hyrax.de.yml +172 -54
  437. data/config/locales/hyrax.en.yml +183 -66
  438. data/config/locales/hyrax.es.yml +174 -49
  439. data/config/locales/hyrax.fr.yml +169 -45
  440. data/config/locales/hyrax.it.yml +172 -48
  441. data/config/locales/hyrax.pt-BR.yml +172 -54
  442. data/config/locales/hyrax.zh.yml +145 -21
  443. data/config/metadata/basic_metadata.yaml +66 -0
  444. data/config/metadata/batch_edit_metadata.yaml +119 -0
  445. data/config/metadata/file_set_metadata.yaml +19 -0
  446. data/config/metadata/redirects.yaml +15 -0
  447. data/config/metadata_profiles/m3_json_schema.json +487 -0
  448. data/config/metadata_profiles/m3_profile.yaml +954 -0
  449. data/config/routes.rb +9 -0
  450. data/docker-compose-allinson.yml +176 -0
  451. data/docker-compose-dassie.yml +14 -6
  452. data/docker-compose-koppie.yml +18 -10
  453. data/docker-compose-sirenia.yml +20 -10
  454. data/documentation/copy_permalink.md +29 -0
  455. data/documentation/developing-your-hyrax-based-app.md +2 -2
  456. data/documentation/file-ingest-characterization.md +763 -0
  457. data/documentation/flexible_metadata.md +113 -0
  458. data/documentation/forms/field_behaviors.md +297 -0
  459. data/documentation/redirects.md +428 -0
  460. data/hyrax.gemspec +7 -2
  461. data/lib/freyja/metadata_adapter.rb +1 -1
  462. data/lib/freyja/persister.rb +25 -1
  463. data/lib/generators/hyrax/collection_resource/collection_resource_generator.rb +2 -2
  464. data/lib/generators/hyrax/collection_resource/templates/collection.rb.erb +5 -1
  465. data/lib/generators/hyrax/collection_resource/templates/collection_form.rb.erb +5 -1
  466. data/lib/generators/hyrax/collection_resource/templates/collection_indexer.rb.erb +6 -1
  467. data/lib/generators/hyrax/iiif_viewer/USAGE +22 -0
  468. data/lib/generators/hyrax/iiif_viewer/iiif_viewer_generator.rb +49 -0
  469. data/lib/generators/hyrax/iiif_viewer/templates/_clover.html.erb +7 -0
  470. data/lib/generators/hyrax/iiif_viewer/templates/clover/clover.css +3328 -0
  471. data/lib/generators/hyrax/iiif_viewer/templates/clover/clover.html +16 -0
  472. data/lib/generators/hyrax/iiif_viewer/templates/clover/clover.js +237 -0
  473. data/lib/generators/hyrax/install_generator.rb +25 -1
  474. data/lib/generators/hyrax/riiif_generator.rb +1 -1
  475. data/lib/generators/hyrax/templates/app/views/blacklight_dynamic_sitemap/sitemap/show.xml.builder +22 -0
  476. data/lib/generators/hyrax/templates/config/clamav.rb +2 -0
  477. data/lib/generators/hyrax/templates/config/initializers/riiif.rb +4 -80
  478. data/lib/generators/hyrax/templates/db/migrate/20240606205215_create_hyrax_flexible_schemas.rb.erb +9 -0
  479. data/lib/generators/hyrax/templates/db/migrate/20240606205216_add_contexts_to_hyrax_flexible_schemas.rb.erb +5 -0
  480. data/lib/generators/hyrax/templates/db/migrate/20260430000001_create_hyrax_redirect_paths.rb.erb +22 -0
  481. data/lib/generators/hyrax/templates/db/migrate/20260521003627_add_additional_redirect_info.rb.erb +39 -0
  482. data/lib/generators/hyrax/templates/package.json +1 -1
  483. data/lib/generators/hyrax/templates/uv.html +60 -82
  484. data/lib/generators/hyrax/work/templates/indexer.rb.erb +1 -1
  485. data/lib/generators/hyrax/work/templates/model.rb.erb +5 -1
  486. data/lib/generators/hyrax/work_resource/templates/form.rb.erb +5 -2
  487. data/lib/generators/hyrax/work_resource/templates/indexer.rb.erb +6 -2
  488. data/lib/generators/hyrax/work_resource/templates/work.rb.erb +5 -2
  489. data/lib/generators/hyrax/work_resource/work_resource_generator.rb +1 -0
  490. data/lib/hyrax/active_fedora_dummy_model.rb +6 -0
  491. data/lib/hyrax/configuration.rb +197 -3
  492. data/lib/hyrax/deprecation.rb +31 -0
  493. data/lib/hyrax/engine.rb +2 -0
  494. data/lib/hyrax/form_fields.rb +17 -5
  495. data/lib/hyrax/indexer.rb +31 -2
  496. data/lib/hyrax/resource_sync/resource_list_writer.rb +3 -0
  497. data/lib/hyrax/schema.rb +15 -4
  498. data/lib/hyrax/specs/shared_specs/factories/administrative_sets.rb +12 -0
  499. data/lib/hyrax/specs/shared_specs/factories/hyrax_embargo.rb +1 -1
  500. data/lib/hyrax/specs/shared_specs/factories/hyrax_file_set.rb +13 -0
  501. data/lib/hyrax/specs/shared_specs/factories/hyrax_lease.rb +5 -1
  502. data/lib/hyrax/specs/shared_specs/factories/hyrax_work.rb +14 -0
  503. data/lib/hyrax/specs/shared_specs/simple_work.rb +41 -0
  504. data/lib/hyrax/specs/shared_specs.rb +1 -0
  505. data/lib/hyrax/transactions/collection_create.rb +2 -1
  506. data/lib/hyrax/transactions/collection_destroy.rb +1 -0
  507. data/lib/hyrax/transactions/collection_update.rb +2 -1
  508. data/lib/hyrax/transactions/container.rb +28 -0
  509. data/lib/hyrax/transactions/steps/apply_permission_template_on_update.rb +65 -0
  510. data/lib/hyrax/transactions/steps/apply_workflow_on_admin_set_change.rb +97 -0
  511. data/lib/hyrax/transactions/steps/ensure_admin_set.rb +1 -1
  512. data/lib/hyrax/transactions/steps/remove_redirect_paths.rb +35 -0
  513. data/lib/hyrax/transactions/steps/save.rb +10 -0
  514. data/lib/hyrax/transactions/steps/set_default_admin_set.rb +1 -1
  515. data/lib/hyrax/transactions/steps/sync_redirect_paths.rb +141 -0
  516. data/lib/hyrax/transactions/work_create.rb +2 -1
  517. data/lib/hyrax/transactions/work_destroy.rb +1 -0
  518. data/lib/hyrax/transactions/work_update.rb +4 -1
  519. data/lib/hyrax/version.rb +1 -1
  520. data/lib/hyrax.rb +61 -0
  521. data/lib/tasks/m3_profile_cleanup.rake +35 -0
  522. data/template.rb +4 -1
  523. metadata +313 -19
  524. data/.dassie/public/robots.txt +0 -1
  525. data/.koppie/public/robots.txt +0 -1
  526. data/app/forms/concerns/hyrax/basic_metadata_form_fields_behavior.rb +0 -39
  527. data/app/views/catalog/_thumbnail_list_collection.html.erb +0 -4
@@ -0,0 +1,763 @@
1
+ # File Ingest and Characterization in Hyrax
2
+
3
+ This document describes how files are ingested, characterized, and processed in Hyrax, with explicit coverage of both the **ActiveFedora** (legacy actor-stack) path and the **Valkyrie** (resource/transaction) path. It is intended for developers who need to trace, debug, or modify these flows.
4
+
5
+ > **Scope:** Hyrax supports two persistence back-ends. The _ActiveFedora_ path stores files directly in a Fedora repository via Hydra/LDP. The _Valkyrie_ path stores files via a pluggable `Valkyrie::StorageAdapter` and persists metadata through the Valkyrie persister/query-service stack. Wings is the compatibility shim that lets ActiveFedora objects participate in Valkyrie workflows.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ 1. [Shared Concepts](#1-shared-concepts)
12
+ 2. [ActiveFedora Ingest Path](#2-activefedora-ingest-path)
13
+ - [Actor Stack Entry Point](#21-actor-stack-entry-point)
14
+ - [File Attachment Job](#22-file-attachment-job)
15
+ - [FileSet and FileActor](#23-fileset-and-fileactor)
16
+ - [IngestJob and JobIoWrapper](#24-ingestjob-and-jobiowrapper)
17
+ 3. [Valkyrie Ingest Path](#3-valkyrie-ingest-path)
18
+ - [Transaction Entry Point](#31-transaction-entry-point)
19
+ - [WorkUploadsHandler](#32-workuploadshandler)
20
+ - [ValkyrieIngestJob and ValkyrieUpload](#33-valkyrieingestjob-and-valkyrieupload)
21
+ 4. [File Characterization](#4-file-characterization)
22
+ - [ActiveFedora Characterization](#41-activefedora-characterization)
23
+ - [Valkyrie Characterization](#42-valkyrie-characterization)
24
+ 5. [Derivative Generation](#5-derivative-generation)
25
+ - [ActiveFedora Derivatives](#51-activefedora-derivatives)
26
+ - [Valkyrie Derivatives](#52-valkyrie-derivatives)
27
+ - [Derivative Service Factory](#53-derivative-service-factory)
28
+ 6. [Event System (The Connective Tissue)](#6-event-system-the-connective-tissue)
29
+ 7. [Metadata Persistence and Indexing](#7-metadata-persistence-and-indexing)
30
+ 8. [Wings: The AF ↔ Valkyrie Bridge](#8-wings-the-af--valkyrie-bridge)
31
+ 9. [Key Configuration Points](#9-key-configuration-points)
32
+ 10. [Developer Cautions](#10-developer-cautions)
33
+
34
+ ---
35
+
36
+ ## 1. Shared Concepts
37
+
38
+ ### Hyrax::UploadedFile
39
+
40
+ `app/models/hyrax/uploaded_file.rb`
41
+
42
+ All file uploads, regardless of back-end, start as an `Hyrax::UploadedFile` ActiveRecord record. CarrierWave mounts a `UploadedFileUploader` on the `:file` column.
43
+
44
+ ```ruby
45
+ # Relates uploaded file to a file set after creation
46
+ def add_file_set!(file_set)
47
+ # Sets file_set_uri based on whether file_set is AF or Valkyrie
48
+ end
49
+ ```
50
+
51
+ Key columns: `file` (CarrierWave uploader), `user_id`, `file_set_uri`.
52
+
53
+ The `file_set_uri` is used for idempotency: both the AF and Valkyrie paths skip files whose `file_set_uri` is already set, preventing duplicate ingest on job retries.
54
+
55
+ ### JobIoWrapper
56
+
57
+ `app/models/job_io_wrapper.rb`
58
+
59
+ A serializable ActiveRecord shim that wraps a file so it can be passed to ActiveJob without losing file-system context. Used exclusively in the AF path.
60
+
61
+ Key fields: `path`, `file_set_id`, `relation` (`:original_file`, etc.), `original_name`, `mime_type`, `uploaded_file_id`.
62
+
63
+ ```ruby
64
+ # Factory used by FileSetActor
65
+ JobIoWrapper.create_with_varied_file_handling!(
66
+ user:, file:, relation:, file_set:, use_valkyrie:
67
+ )
68
+
69
+ # Called by IngestJob
70
+ wrapper.ingest_file # delegates to FileActor#ingest_file
71
+ ```
72
+
73
+ ### Hyrax::FileMetadata (Valkyrie only)
74
+
75
+ `app/models/hyrax/file_metadata.rb`
76
+
77
+ A `Valkyrie::Resource` that records metadata about a single file attached to a FileSet. There is no AF equivalent; AF stores file properties directly on the `Hydra::PCDM::File` LDP resource.
78
+
79
+ Key attributes: `file_identifier` (Valkyrie storage-adapter ID), `file_set_id`, `pcdm_use` (array of `RDF::URI` use constants from `Hyrax::FileMetadata::Use`), `mime_type`, `original_filename`, `recorded_size`, characterization properties (`height`, `width`, `file_size`, `format_label`, etc.).
80
+
81
+ Use constants (defined under `Hyrax::FileMetadata::Use`):
82
+
83
+ | Constant | Purpose |
84
+ |----------|---------|
85
+ | `ORIGINAL_FILE` | The uploaded/original file |
86
+ | `THUMBNAIL_IMAGE` | Thumbnail derivative |
87
+ | `EXTRACTED_TEXT` | Full-text extraction |
88
+ | `INTERMEDIATE_FILE` | Intermediate processing copy |
89
+ | `PRESERVATION_FILE` | Long-term preservation copy |
90
+ | `SERVICE_FILE` | Access/service copy |
91
+ | `TRANSCRIPT` | Transcript |
92
+
93
+ ---
94
+
95
+ ## 2. ActiveFedora Ingest Path
96
+
97
+ ### 2.1 Actor Stack Entry Point
98
+
99
+ `app/services/hyrax/default_middleware_stack.rb`
100
+
101
+ The ActorStack is a middleware chain that processes work create/update operations. Each actor calls `next_actor.create(env)` (or `update`) to pass control down the chain.
102
+
103
+ Relevant actors in default order:
104
+
105
+ ```
106
+ Hyrax::Actors::OptimisticLockValidator
107
+ Hyrax::Actors::CreateWithRemoteFilesActor
108
+ Hyrax::Actors::CreateWithFilesActor ← triggers file attachment
109
+ Hyrax::Actors::CollectionsMembershipActor
110
+ Hyrax::Actors::AddToWorkActor
111
+ Hyrax::Actors::AttachMembersActor
112
+ Hyrax::Actors::ApplyOrderActor
113
+ Hyrax::Actors::DefaultAdminSetActor
114
+ Hyrax::Actors::InterpretVisibilityActor
115
+ Hyrax::Actors::TransferRequestActor
116
+ Hyrax::Actors::ApplyPermissionTemplateActor
117
+ Hyrax::Actors::CleanupFileSetsActor
118
+ Hyrax::Actors::CleanupTrophiesActor
119
+ Hyrax::Actors::FeaturedWorkActor
120
+ Hyrax::Actors::ModelActor ← final AF persistence
121
+ ```
122
+
123
+ `app/actors/hyrax/actors/create_with_files_actor.rb`
124
+
125
+ `CreateWithFilesActor` extracts `uploaded_file_ids` from `env.attributes`, validates all are `Hyrax::UploadedFile` records, calls the next actor to persist the work, and then enqueues `AttachFilesToWorkJob`:
126
+
127
+ ```ruby
128
+ def create(env)
129
+ uploaded_file_ids = env.attributes.delete(:uploaded_files)
130
+ # ... validation ...
131
+ next_actor.create(env) &&
132
+ AttachFilesToWorkJob.perform_later(env.curation_concern, uploaded_files)
133
+ end
134
+ ```
135
+
136
+ ### 2.2 File Attachment Job
137
+
138
+ `app/jobs/attach_files_to_work_job.rb`
139
+
140
+ `AttachFilesToWorkJob` is the routing point between AF and Valkyrie:
141
+
142
+ ```ruby
143
+ def perform(work, uploaded_files, **work_attributes)
144
+ case work
145
+ when ActiveFedora::Base
146
+ perform_af(work, uploaded_files, work_attributes) # AF path
147
+ else
148
+ Hyrax::WorkUploadsHandler.new(work: work) # Valkyrie path
149
+ .add(files: uploaded_files)
150
+ .attach
151
+ end
152
+ end
153
+ ```
154
+
155
+ The AF branch (`perform_af`) loops over `uploaded_files`, skipping any with an existing `file_set_uri`, and for each remaining file:
156
+
157
+ 1. Instantiates a new `FileSet` (AF model) and a `Hyrax::Actors::FileSetActor`.
158
+ 2. Calls `actor.create_metadata(visibility_attrs)` — sets depositor, dates, visibility.
159
+ 3. Calls `actor.create_content(uploaded_file)` — queues `IngestJob`.
160
+ 4. Calls `actor.attach_to_work(work, metadata)` — acquires a lock, appends the FileSet to `work.ordered_members`, and fires the `:after_create_fileset` callback.
161
+
162
+ ### 2.3 FileSet and FileActor
163
+
164
+ `app/actors/hyrax/actors/file_set_actor.rb`
165
+
166
+ ```ruby
167
+ # Creates metadata on the file_set and saves it
168
+ def create_metadata(file_set_params = {})
169
+ file_set.depositor = user.user_key
170
+ # ... dates, creator, visibility via actor stack ...
171
+ end
172
+
173
+ # Queues ingest for the file
174
+ def create_content(file, relation = :original_file, from_url: false)
175
+ # sets label/title from filename
176
+ # saves file_set
177
+ # unless from_url: IngestJob.perform_later(wrapper)
178
+ # if from_url: FileActor.ingest_file(file); then visibility/permission jobs
179
+ end
180
+
181
+ # Appends file_set to work with lock
182
+ def attach_to_work(work, file_set_params = {})
183
+ acquire_lock_for(work.id) do
184
+ work.ordered_members << file_set
185
+ work.representative_id ||= file_set.id
186
+ work.thumbnail_id ||= file_set.id
187
+ work.save!
188
+ end
189
+ Hyrax.publisher.publish('file.set.attached', file_set: file_set, user: user)
190
+ end
191
+ ```
192
+
193
+ `app/actors/hyrax/actors/file_actor.rb`
194
+
195
+ `FileActor` performs the actual LDP write:
196
+
197
+ ```ruby
198
+ def ingest_file(io)
199
+ io.use_valkyrie = false # forces AF objects even in mixed environments
200
+
201
+ Hydra::Works::AddFileToFileSet.call(file_set, io, relation, versioning: false)
202
+ return false unless file_set.save
203
+
204
+ repository_file = related_file # the Hydra::PCDM::File LDP node
205
+ create_version(repository_file, user) # via Hyrax::VersioningService
206
+ CharacterizeJob.perform_later(file_set, repository_file.id, pathhint(io))
207
+ end
208
+ ```
209
+
210
+ `Hydra::Works::AddFileToFileSet` is a `hydra-works` gem operation that streams the file content to Fedora via LDP.
211
+
212
+ ### 2.4 IngestJob and JobIoWrapper
213
+
214
+ `app/jobs/ingest_job.rb`
215
+
216
+ ```ruby
217
+ class IngestJob < Hyrax::ApplicationJob
218
+ queue_as Hyrax.config.ingest_queue_name
219
+
220
+ def perform(wrapper, notification: false)
221
+ wrapper.ingest_file # calls FileActor#ingest_file → CharacterizeJob
222
+ ContentNewVersionEventJob.perform_later(wrapper.file_set_id, ...) if notification
223
+ end
224
+ end
225
+ ```
226
+
227
+ `IngestJob` accepts a `JobIoWrapper`, which is serialized to the job queue. `wrapper.ingest_file` delegates back to `Hyrax::Actors::FileActor#ingest_file`.
228
+
229
+ **Full AF ingest sequence:**
230
+
231
+ ```
232
+ CreateWithFilesActor#create
233
+ └─ AttachFilesToWorkJob (async)
234
+ └─ FileSetActor#create_metadata
235
+ └─ FileSetActor#create_content
236
+ └─ IngestJob (async)
237
+ └─ JobIoWrapper#ingest_file
238
+ └─ FileActor#ingest_file
239
+ └─ Hydra::Works::AddFileToFileSet (writes to Fedora)
240
+ └─ VersioningService#create
241
+ └─ CharacterizeJob (async) → see §4.1
242
+ ```
243
+
244
+ ---
245
+
246
+ ## 3. Valkyrie Ingest Path
247
+
248
+ ### 3.1 Transaction Entry Point
249
+
250
+ `lib/hyrax/transactions/work_create.rb`
251
+
252
+ The Valkyrie path uses `Dry::Transaction`-style step chains instead of an actor stack.
253
+
254
+ Default steps for `Hyrax::Transactions::WorkCreate`:
255
+
256
+ ```
257
+ change_set.set_default_admin_set
258
+ change_set.ensure_admin_set
259
+ change_set.set_user_as_depositor
260
+ change_set.apply
261
+ work_resource.apply_permission_template
262
+ work_resource.save_acl
263
+ work_resource.add_file_sets ← file handling
264
+ work_resource.change_depositor
265
+ work_resource.add_to_parent
266
+ work_resource.sync_redirect_paths
267
+ ```
268
+
269
+ `lib/hyrax/transactions/steps/add_file_sets.rb`
270
+
271
+ ```ruby
272
+ def call(obj, uploaded_files: [], file_set_params: [])
273
+ # delegates entirely to WorkUploadsHandler
274
+ Hyrax::WorkUploadsHandler.new(work: obj)
275
+ .add(files: uploaded_files, file_set_params: file_set_params)
276
+ .attach
277
+ # then propagates embargo/lease to file_sets
278
+ Success(obj)
279
+ end
280
+ ```
281
+
282
+ ### 3.2 WorkUploadsHandler
283
+
284
+ `app/services/hyrax/work_uploads_handler.rb`
285
+
286
+ This is the Valkyrie orchestrator. Its public API is:
287
+
288
+ ```ruby
289
+ Hyrax::WorkUploadsHandler.new(work: my_work)
290
+ .add(files: [file1, file2], file_set_params: [...])
291
+ .attach
292
+ ```
293
+
294
+ `add` validates all files are `Hyrax::UploadedFile` and discards those with an existing `file_set_uri` (idempotency guard).
295
+
296
+ `attach` acquires a Redis/Redlock lock on the work ID, then for each file:
297
+
298
+ ```ruby
299
+ # private make_file_set_and_ingest
300
+ def make_file_set_and_ingest(file, file_set_params = {})
301
+ file_set = @persister.save(
302
+ resource: Hyrax.config.valkyrie_file_set_class.new(file_set_args(file, file_set_params))
303
+ )
304
+ Hyrax.publisher.publish('object.deposited', object: file_set, user: file.user)
305
+ file.add_file_set!(file_set) # sets UploadedFile#file_set_uri
306
+ Hyrax::AccessControlList.copy_permissions(source: work_acl, target: file_set)
307
+ append_to_work(file_set) # work.member_ids << file_set.id
308
+ { file_set: file_set, user: file.user, job: ValkyrieIngestJob.new(file) }
309
+ end
310
+ ```
311
+
312
+ After all file_sets are created, `attach` saves the work (with updated `member_ids`), publishes `'object.metadata.updated'`, then enqueues each `ValkyrieIngestJob` and publishes `'file.set.attached'`.
313
+
314
+ ### 3.3 ValkyrieIngestJob and ValkyrieUpload
315
+
316
+ `app/jobs/valkyrie_ingest_job.rb`
317
+
318
+ ```ruby
319
+ class ValkyrieIngestJob < Hyrax::ApplicationJob
320
+ queue_as Hyrax.config.ingest_queue_name
321
+
322
+ def perform(file, pcdm_use: Hyrax::FileMetadata::Use::ORIGINAL_FILE)
323
+ file_set = Hyrax.query_service.find_by(id: Valkyrie::ID.new(file.file_set_uri))
324
+ upload_file(file: file, file_set: file_set, pcdm_use: pcdm_use, user: file.user)
325
+ end
326
+ end
327
+ ```
328
+
329
+ `upload_file` calls `Hyrax::ValkyrieUpload.file(...)`.
330
+
331
+ `app/services/hyrax/valkyrie_upload.rb`
332
+
333
+ `ValkyrieUpload` is the Valkyrie equivalent of `FileActor#ingest_file`:
334
+
335
+ ```ruby
336
+ def upload(io:, filename:, file_set:, pcdm_use:, use_valkyrie: Hyrax.config.use_valkyrie?)
337
+ # 1. Handle versioning if storage adapter supports it
338
+ if pcdm_use == ORIGINAL_FILE && file_set.original_file_id && adapter.supports?(:versions)
339
+ streamfile = adapter.upload_version(id: file_set.original_file_id, file: io)
340
+ else
341
+ streamfile = adapter.upload(file: io, original_filename: filename, resource: file_set)
342
+ end
343
+
344
+ # 2. Create and persist FileMetadata
345
+ file_metadata = Hyrax::FileMetadata.new(
346
+ file_identifier: streamfile.id,
347
+ file_set_id: file_set.id,
348
+ pcdm_use: [pcdm_use],
349
+ mime_type: streamfile.content_type,
350
+ original_filename: filename,
351
+ recorded_size: [streamfile.size]
352
+ )
353
+ Hyrax.persister.save(resource: file_metadata)
354
+
355
+ # 3. Update file_set.file_ids
356
+ file_set.file_ids += [file_metadata.id]
357
+ # (title/label also set from filename)
358
+ Hyrax.persister.save(resource: file_set)
359
+
360
+ # 4. Publish events (triggers characterization via FileListener)
361
+ Hyrax.publisher.publish('file.uploaded', metadata: file_metadata)
362
+ Hyrax.publisher.publish('file.metadata.updated', metadata: file_metadata, user: user)
363
+ Hyrax.publisher.publish('object.membership.updated', object: file_set, user: user)
364
+ end
365
+ ```
366
+
367
+ The storage adapter (`Hyrax.storage_adapter`) is configurable — common choices are the disk adapter (development), an S3 adapter, or a Fedora 6 adapter via the Valkyrie Fedora gem.
368
+
369
+ **Full Valkyrie ingest sequence:**
370
+
371
+ ```
372
+ WorkCreate transaction
373
+ └─ Steps::AddFileSets
374
+ └─ WorkUploadsHandler#attach (with Redis lock)
375
+ └─ persister.save(FileSet) (creates Valkyrie FileSet)
376
+ └─ AccessControlList.copy_permissions
377
+ └─ ValkyrieIngestJob (enqueued)
378
+ └─ ValkyrieUpload#upload
379
+ └─ storage_adapter.upload (writes bytes to storage backend)
380
+ └─ persister.save(FileMetadata)
381
+ └─ persister.save(FileSet)
382
+ └─ publisher.publish('file.uploaded') → FileListener → ValkyrieCharacterizationJob
383
+ ```
384
+
385
+ ---
386
+
387
+ ## 4. File Characterization
388
+
389
+ Characterization extracts technical metadata (MIME type, dimensions, checksum, format label, etc.) from the stored file. Both paths ultimately rely on FITS (via `Hydra::FileCharacterization` and `Hydra::Works::CharacterizationService`), but they wrap it differently.
390
+
391
+ ### 4.1 ActiveFedora Characterization
392
+
393
+ `app/jobs/characterize_job.rb`
394
+
395
+ Triggered directly from `FileActor#ingest_file`:
396
+
397
+ ```ruby
398
+ CharacterizeJob.perform_later(file_set, repository_file.id, pathhint)
399
+ ```
400
+
401
+ ```ruby
402
+ def perform(file_set, file_id, filepath = nil)
403
+ # Ensure file is on disk (downloads from Fedora if needed)
404
+ filepath ||= Hyrax::WorkingDirectory.find_or_retrieve(file_id, file_set.id)
405
+ return unless file_set.characterization_proxy?
406
+ characterize(file_set, file_id, filepath)
407
+ Hyrax.publisher.publish('file.characterized', file_set: file_set,
408
+ file_id: file_id, path_hint: filepath)
409
+ end
410
+
411
+ def characterize(file_set, _file_id, filepath)
412
+ # Clear old values first (important — see §10)
413
+ clear_metadata(file_set)
414
+
415
+ # Run FITS via Hydra::Works
416
+ CharacterizeJob.characterization_service.run(
417
+ file_set.characterization_proxy, filepath, **Hyrax.config.characterization_options
418
+ )
419
+
420
+ file_set.characterization_proxy.save!
421
+ file_set.update_index
422
+ file_set.save # persists date_modified etc.
423
+ end
424
+ ```
425
+
426
+ Default `characterization_service` is `Hydra::Works::CharacterizationService`. The FITS result is parsed and properties are set directly on the `Hydra::PCDM::File` LDP object (the `characterization_proxy`).
427
+
428
+ Properties written to the proxy include: `format_label`, `file_size`, `height`, `width`, `filename`, `well_formed`, `valid`, `date_created`, `fits_version`, `exif_version`, `checksum`, `character_set`, `markup_basis`, `markup_language`, `byte_order`, `compression`, `color_space`, `profile_name`, `profile_version`, `orientation`, `color_map`, `image_producer`, `capture_device`, `scanning_software`, `gps_timestamp`, `latitude`, `longitude`, `file_title`, `creator`, `page_count`, `language`, `word_count`, `character_count`, `line_count`, `graphics_count`, `data_format`, `offset`, `alpha_channels` (if IIIF enabled), `duration`, `sample_rate`, and others.
429
+
430
+ ### 4.2 Valkyrie Characterization
431
+
432
+ `app/jobs/valkyrie_characterization_job.rb`
433
+
434
+ Triggered by the event `'file.uploaded'` via `FileListener`:
435
+
436
+ ```ruby
437
+ class ValkyrieCharacterizationJob < Hyrax::ApplicationJob
438
+ queue_as Hyrax.config.ingest_queue_name
439
+
440
+ def perform(file_metadata_id)
441
+ metadata = Hyrax.custom_queries.find_file_metadata_by(id: file_metadata_id)
442
+ Hyrax.config.characterization_service.run(
443
+ metadata: metadata,
444
+ file: metadata.file, # retrieved from storage_adapter
445
+ user: metadata.file_set.depositor
446
+ )
447
+ end
448
+ end
449
+ ```
450
+
451
+ `app/services/hyrax/characterization/valkyrie_characterization_service.rb`
452
+
453
+ ```ruby
454
+ def self.run(metadata:, file:, user:, **options)
455
+ new(metadata, file, user, **options).tap do |service|
456
+ service.characterize
457
+ Hyrax.persister.save(resource: metadata)
458
+ Hyrax.publisher.publish('file.metadata.updated', metadata: metadata, user: user)
459
+ Hyrax.publisher.publish('file.characterized',
460
+ file_set: metadata.file_set, file_id: metadata.id.to_s, path_hint: file.disk_path)
461
+ end
462
+ end
463
+ ```
464
+
465
+ Internally uses the same FITS toolchain (`Hydra::FileCharacterization`) but maps results to `FileMetadata` attributes rather than to PCDM file properties. Key differences:
466
+
467
+ - `mime_type` is set to the **last** value returned (FITS may return multiple).
468
+ - `height` and `width` are set to the **maximum** value (handles multi-page documents).
469
+ - Results are persisted via `Hyrax.persister.save(resource: metadata)`.
470
+
471
+ Configurable via `Hyrax.config.characterization_service` (replace the default to use custom characterizers such as MediaInfo for AV files or Tika for documents).
472
+
473
+ ---
474
+
475
+ ## 5. Derivative Generation
476
+
477
+ Derivatives (thumbnails, web-friendly formats, extracted text) are created after characterization.
478
+
479
+ ### 5.1 ActiveFedora Derivatives
480
+
481
+ `app/jobs/create_derivatives_job.rb`
482
+
483
+ Triggered by `'file.characterized'` event via `FileListener`:
484
+
485
+ ```ruby
486
+ def perform(file_set, file_id, filepath = nil)
487
+ return if Hyrax.config.disable_ffmpeg_check? # video guard
488
+ filepath ||= Hyrax::WorkingDirectory.find_or_retrieve(file_id, file_set.id)
489
+ file_set.create_derivatives(filepath) # delegates to FileSetDerivativesService
490
+ file_set.reload
491
+ file_set.update_index
492
+ work = file_set.parent
493
+ work&.update_index if file_set.id == work&.thumbnail_id
494
+ end
495
+ ```
496
+
497
+ `file_set.create_derivatives(filepath)` calls `Hyrax::FileSetDerivativesService#create_derivatives`, which dispatches by MIME type to format-specific processors (see §5.3).
498
+
499
+ ### 5.2 Valkyrie Derivatives
500
+
501
+ `app/jobs/valkyrie_create_derivatives_job.rb`
502
+
503
+ Triggered by `'file.characterized'` event via `FileListener`:
504
+
505
+ ```ruby
506
+ def perform(file_set_id, file_id, _filepath = nil)
507
+ return if Hyrax.config.disable_ffmpeg_check?
508
+ file_metadata = Hyrax.custom_queries.find_file_metadata_by(id: file_id)
509
+ file = Hyrax.storage_adapter.find_by(id: file_metadata.file_identifier)
510
+ Hyrax::DerivativeService.for(file_metadata).create_derivatives(file.disk_path)
511
+
512
+ # Reindex parent work if this file set is the thumbnail
513
+ reindex_parent(file_set_id)
514
+ end
515
+ ```
516
+
517
+ The key difference: AF passes the `FileSet` object; Valkyrie passes a `FileMetadata` to the derivative service factory.
518
+
519
+ ### 5.3 Derivative Service Factory
520
+
521
+ `app/services/hyrax/derivative_service.rb`
522
+
523
+ ```ruby
524
+ def self.for(file_set, services: Hyrax.config.derivative_services)
525
+ services.each do |service_class|
526
+ service = service_class.new(file_set)
527
+ return service if service.valid?
528
+ end
529
+ new(file_set) # fallback to base service
530
+ end
531
+ ```
532
+
533
+ Each service implements `#valid?` (checks MIME type), `#create_derivatives(filename)`, and `#cleanup_derivatives`.
534
+
535
+ `app/services/hyrax/file_set_derivatives_service.rb` — the default service:
536
+
537
+ | MIME type group | Derivatives created |
538
+ |-----------------|---------------------|
539
+ | `application/pdf` | Thumbnail (ImageMagick), full-text extraction (Solr) |
540
+ | Office documents (`application/msword`, etc.) | Thumbnail (LibreOffice → ImageMagick), full-text |
541
+ | Audio (`audio/*`) | MP3 (FFmpeg), OGG (FFmpeg) |
542
+ | Video (`video/*`) | JPG thumbnail (FFmpeg), WebM (FFmpeg), MP4 (FFmpeg) |
543
+ | Image (`image/*`) | JPG thumbnail (ImageMagick, layer 0 for pyramidal TIFF) |
544
+
545
+ Derivatives are stored at paths derived from `Hyrax::DerivativePath.derivative_path_for_reference`.
546
+
547
+ ---
548
+
549
+ ## 6. Event System (The Connective Tissue)
550
+
551
+ Hyrax uses `Dry::Events` (via `Hyrax::Publisher`) to decouple ingest stages. Listeners are registered in `config/initializers/listeners.rb`.
552
+
553
+ ### Events fired during ingest
554
+
555
+ | Event | Published by | Consumed by |
556
+ |-------|-------------|-------------|
557
+ | `'object.deposited'` | `WorkUploadsHandler` | Analytics, notifications |
558
+ | `'file.set.attached'` | `WorkUploadsHandler`, `FileSetActor#attach_to_work` | Analytics, notifications |
559
+ | `'file.uploaded'` | `ValkyrieUpload#upload` | `FileListener` → `ValkyrieCharacterizationJob` |
560
+ | `'file.metadata.updated'` | `ValkyrieUpload#upload`, `ValkyrieCharacterizationService` | `MetadataIndexListener` → Solr reindex |
561
+ | `'object.membership.updated'` | `ValkyrieUpload#upload` | `MetadataIndexListener` → Solr reindex |
562
+ | `'object.metadata.updated'` | `WorkUploadsHandler#attach` | `MetadataIndexListener` → Solr reindex |
563
+ | `'file.characterized'` | `CharacterizeJob`, `ValkyrieCharacterizationService` | `FileListener` → `Create[Valkyrie]DerivativesJob` |
564
+
565
+ ### FileListener dispatch (AF vs Valkyrie)
566
+
567
+ `app/services/hyrax/listeners/file_listener.rb`
568
+
569
+ ```ruby
570
+ def on_file_characterized(event)
571
+ case event[:file_set]
572
+ when ActiveFedora::Base
573
+ CreateDerivativesJob.perform_later(event[:file_set], event[:file_id], event[:path_hint])
574
+ else
575
+ ValkyrieCreateDerivativesJob.perform_later(event[:file_set].id.to_s, event[:file_id])
576
+ end
577
+ end
578
+
579
+ def on_file_uploaded(event)
580
+ # Only for original files; respects skip_derivatives flag
581
+ return if event.payload[:skip_derivatives] || !event[:metadata]&.original_file?
582
+ ValkyrieCharacterizationJob.perform_later(event[:metadata].id.to_s)
583
+ end
584
+ ```
585
+
586
+ `on_file_uploaded` is Valkyrie-only; the AF path triggers `CharacterizeJob` directly from `FileActor#ingest_file` without an event.
587
+
588
+ ---
589
+
590
+ ## 7. Metadata Persistence and Indexing
591
+
592
+ ### ActiveFedora
593
+
594
+ - File properties live on the `Hydra::PCDM::File` LDP resource (a node within the FileSet in Fedora).
595
+ - `CharacterizeJob` writes properties directly to `file_set.characterization_proxy` and calls `file_set.update_index` to push to Solr.
596
+ - `CreateDerivativesJob` calls `file_set.reload` + `file_set.update_index` after creating derivatives.
597
+
598
+ ### Valkyrie
599
+
600
+ - File properties live on `Hyrax::FileMetadata` resources persisted via `Hyrax.persister`.
601
+ - Solr indexing is event-driven: the `'file.metadata.updated'` event is consumed by `Hyrax::Listeners::MetadataIndexListener`.
602
+
603
+ `app/services/hyrax/listeners/metadata_index_listener.rb`
604
+
605
+ ```ruby
606
+ def on_file_metadata_updated(event)
607
+ Hyrax.index_adapter.save(resource: event[:metadata])
608
+ # If this is the original file, reindex the parent file_set too
609
+ if event[:metadata].original_file?
610
+ file_set = Hyrax.query_service.find_by(id: event[:metadata].file_set_id)
611
+ Hyrax.index_adapter.save(resource: file_set)
612
+ end
613
+ end
614
+
615
+ def on_object_metadata_updated(event)
616
+ Hyrax.index_adapter.save(resource: event[:object])
617
+ # Reindex parent work if object is a thumbnail file_set
618
+ ...
619
+ end
620
+ ```
621
+
622
+ Custom queries for file lookup:
623
+
624
+ - `Hyrax.custom_queries.find_file_metadata_by(id:)` — single `FileMetadata`
625
+ - `Hyrax.custom_queries.find_many_file_metadata_by_use(resource:, use:)` — files by use predicate
626
+ - `Hyrax.custom_queries.find_original_file(file_set:)` — original file for a FileSet
627
+
628
+ ---
629
+
630
+ ## 8. Wings: The AF ↔ Valkyrie Bridge
631
+
632
+ Wings (`lib/wings/`) is a compatibility adapter that allows AF objects to be used in Valkyrie workflows by wrapping them as `Valkyrie::Resource` instances on the fly. It is used when `Hyrax.config.use_valkyrie?` is true but the application still has AF-backed models.
633
+
634
+ Key components:
635
+
636
+ | File | Purpose |
637
+ |------|---------|
638
+ | `lib/wings/active_fedora_converter/` | Converts AF objects to Valkyrie resources |
639
+ | `lib/wings/valkyrie/` | Reverse: Valkyrie resources back to AF |
640
+ | `lib/wings/model_transformer.rb` | Model class mapping |
641
+ | `lib/wings/transformer_value_mapper.rb` | Property value mapping |
642
+ | `app/actors/hyrax/actors/file_actor.rb` | Explicitly forces `io.use_valkyrie = false` in the AF path to avoid using Valkyrie objects inadvertently |
643
+
644
+ `AttachFilesToWorkJob` performs the `case work` switch: AF objects are an `ActiveFedora::Base` instance; Valkyrie resources are not, even if Wings-wrapped.
645
+
646
+ The `'file.characterized'` event dispatch in `FileListener` uses the same `case` pattern: `when ActiveFedora::Base` routes to `CreateDerivativesJob`; otherwise routes to `ValkyrieCreateDerivativesJob`.
647
+
648
+ ---
649
+
650
+ ## 9. Key Configuration Points
651
+
652
+ `Hyrax.config` controls which path is taken at several junctions. Relevant knobs:
653
+
654
+ | Config key | Type | Purpose |
655
+ |------------|------|---------|
656
+ | `use_valkyrie?` | Boolean | Master switch for Valkyrie path |
657
+ | `valkyrie_file_set_class` | Class | The `Hyrax::FileSet` subclass used by `WorkUploadsHandler` |
658
+ | `ingest_queue_name` | String/Symbol | ActiveJob queue for `IngestJob`, `ValkyrieIngestJob`, characterize, and derivatives jobs |
659
+ | `characterization_service` | Class | Replaced to customise characterization (e.g., swap out FITS) |
660
+ | `characterization_options` | Hash | Extra options forwarded to characterization service |
661
+ | `derivative_services` | Array<Class> | Priority-ordered list of derivative service classes; first `#valid?` wins |
662
+ | `extract_full_text?` | Boolean | Whether to run full-text extraction during derivatives |
663
+ | `enable_ffmpeg` | Boolean | Whether to run video/audio derivatives |
664
+ | `iiif_image_server?` | Boolean | Whether to write `alpha_channels` attribute during AF characterization |
665
+ | `file_set_file_service` | Class | Controls how files are located in the working directory |
666
+
667
+ Valkyrie-specific infrastructure:
668
+
669
+ | Accessor | Purpose |
670
+ |----------|---------|
671
+ | `Hyrax.storage_adapter` | `Valkyrie::StorageAdapter` for file content |
672
+ | `Hyrax.persister` | `Valkyrie::MetadataAdapter::Persister` for metadata |
673
+ | `Hyrax.query_service` | `Valkyrie::MetadataAdapter::QueryService` for reads |
674
+ | `Hyrax.index_adapter` | Solr-backed adapter for search indexing |
675
+ | `Hyrax.custom_queries` | Registry of domain-specific query handlers |
676
+
677
+ ---
678
+
679
+ ## 10. Developer Cautions
680
+
681
+ ### Clearing characterization metadata before re-running FITS
682
+
683
+ `CharacterizeJob#clear_metadata` (AF path) explicitly resets all characterization properties to empty arrays before running FITS. If this is skipped, old values from a previous characterization run can remain and produce incorrect Solr documents. The Valkyrie path (`ValkyrieCharacterizationService#apply_metadata`) overwrites properties, but only those returned by FITS — stale values for _missing_ properties are not automatically cleared.
684
+
685
+ ### `io.use_valkyrie = false` in FileActor
686
+
687
+ `FileActor#ingest_file` hard-codes `io.use_valkyrie = false` before calling `Hydra::Works::AddFileToFileSet`. This is intentional: the AF actor path must write to Fedora LDP regardless of the `Hyrax.config.use_valkyrie?` flag. Removing this line or making it conditional will break the AF ingest path.
688
+
689
+ ### File idempotency guard
690
+
691
+ Both paths skip files whose `Hyrax::UploadedFile#file_set_uri` is already set. This is an incomplete idempotency guard: if a job fails _after_ `add_file_set!` is called but _before_ the file is fully ingested, the record will be skipped on retry and the file will not be re-ingested. Track [hyrax#TODO note in WorkUploadsHandler] when investigating stuck uploads.
692
+
693
+ ### Lock scope
694
+
695
+ `WorkUploadsHandler` (Valkyrie) acquires a single Redlock on the work for the entire batch of file_sets. `FileSetActor#attach_to_work` (AF) acquires a separate lock per file_set attachment. If Redis is unavailable in the AF path, the lock silently degrades (check `Lockable`).
696
+
697
+ ### Derivative ordering vs. characterization
698
+
699
+ Derivatives are triggered by the `'file.characterized'` event, not by `'file.uploaded'`. If characterization is skipped (e.g., via `skip_derivatives: true` on `'file.uploaded'`), derivatives will also be skipped. Some tests and bulk-ingest scripts pass `skip_derivatives: true` for performance; verify this flag is not accidentally retained in production workflows.
700
+
701
+ ### Storage adapter `disk_path` requirement
702
+
703
+ `ValkyrieCreateDerivativesJob` calls `file.disk_path` on the storage-adapter result. Not all storage adapters guarantee a disk path (e.g., a pure-S3 adapter may return a temporary local path or raise). Verify your storage adapter's behaviour before using a non-disk-backed adapter in production; you may need to override the derivatives job to download the file locally first.
704
+
705
+ ### Wings and Valkyrie FileMetadata
706
+
707
+ Wings wraps AF FileSet objects as Valkyrie resources, but the AF path never creates `Hyrax::FileMetadata` records. Queries like `find_file_metadata_by` and `find_original_file` will return nothing for AF-backed file sets unless Wings re-projects the PCDM file graph. Do not mix these query paths when operating on AF works.
708
+
709
+ ### Listener registration order
710
+
711
+ `config/initializers/listeners.rb` registers listeners using `Hyrax.publisher.subscribe`. If you add a custom listener that responds to `'file.uploaded'`, ensure it handles both the Valkyrie case (where `event[:metadata]` is a `Hyrax::FileMetadata`) and the AF case (where this event is never published — AF characterization is triggered synchronously in `FileActor`).
712
+
713
+ ### Transaction steps vs. actor stack customization
714
+
715
+ Adding file-related behaviour to the AF path means modifying or inserting actors in `Hyrax.config.actor_middleware`. Adding the equivalent to the Valkyrie path means adding a step to the `WorkCreate` (or `WorkUpdate`) transaction, or customizing `WorkUploadsHandler`. These are separate extension points; changes to one do not affect the other.
716
+
717
+ ---
718
+
719
+ ## Appendix: Simplified Sequence Diagrams
720
+
721
+ ### ActiveFedora
722
+
723
+ ```
724
+ Browser/API → Work Form
725
+ → CurationConcern::Actor (actor stack)
726
+ → CreateWithFilesActor
727
+ → AttachFilesToWorkJob [async]
728
+ → FileSetActor#create_content
729
+ → IngestJob [async]
730
+ → FileActor#ingest_file
731
+ → Hydra::Works::AddFileToFileSet [Fedora LDP write]
732
+ → VersioningService#create
733
+ → CharacterizeJob [async]
734
+ → Hydra::Works::CharacterizationService [FITS]
735
+ → file_set.update_index [Solr]
736
+ → publisher 'file.characterized'
737
+ → CreateDerivativesJob [async]
738
+ → FileSetDerivativesService
739
+ → file_set.update_index [Solr]
740
+ ```
741
+
742
+ ### Valkyrie
743
+
744
+ ```
745
+ Browser/API → Work Form / Transaction
746
+ → Hyrax::Transactions::WorkCreate
747
+ → Steps::AddFileSets
748
+ → WorkUploadsHandler#attach [Redis lock]
749
+ → persister.save(FileSet)
750
+ → ValkyrieIngestJob [async]
751
+ → ValkyrieUpload#upload
752
+ → storage_adapter.upload [storage backend write]
753
+ → persister.save(FileMetadata)
754
+ → publisher 'file.uploaded'
755
+ → ValkyrieCharacterizationJob [async]
756
+ → ValkyrieCharacterizationService [FITS]
757
+ → persister.save(FileMetadata)
758
+ → publisher 'file.metadata.updated' → MetadataIndexListener → Solr
759
+ → publisher 'file.characterized'
760
+ → ValkyrieCreateDerivativesJob [async]
761
+ → DerivativeService#create_derivatives
762
+ → index_adapter.save(parent work) [Solr]
763
+ ```