diff --git a/app/services/publish_metadata_service.rb b/app/services/publish_metadata_service.rb index 44f2f6ca6..ab0cf9723 100644 --- a/app/services/publish_metadata_service.rb +++ b/app/services/publish_metadata_service.rb @@ -20,6 +20,7 @@ def publish release_tags = ReleaseTags.for(item: item) transfer_metadata(release_tags) + bookkeep_collections publish_notify_on_success end @@ -37,8 +38,61 @@ def transfer_metadata(release_tags) transfer_to_document_store(PublicDescMetadataService.new(item).to_xml, 'mods') end + # Maintain bidirectional symlinks from: + # - an item to the collections it belongs to + # - a collection to the items within + def bookkeep_collections + FileUtils.mkdir_p(item_collections_dir) + existing_collections = Dir.children(item_collections_dir) + + # Write bidirectional symlinks for collection membership + item.collections.each do |coll| + Rails.logger.debug("[Publish][#{item.pid}] Adding collection association with #{coll.pid}") + collection_items_dir = collection_member_dir(coll.pid) + FileUtils.mkdir_p(collection_items_dir) + FileUtils.ln_s(item.content_dir, File.join(collection_items_dir, local_part(item.pid)), force: true) + FileUtils.ln_s(coll.content_dir, File.join(item_collections_dir, local_part(coll.pid)), force: true) + end + + # Remove bidirectional collection membership for collections no longer asserted + (existing_collections - item.collections.map { |coll| local_part(coll.pid) }).each do |coll_pid| + Rails.logger.debug("[Publish][#{item.pid}] Removing collection association with #{coll_pid}") + FileUtils.rm(File.join(item_collections_dir, coll_pid), force: true) + + collection_items_dir = collection_member_dir(coll_pid) + next unless Dir.exist? collection_items_dir + + FileUtils.rm(File.join(collection_items_dir, local_part(item.pid)), force: true) + end + end + + # Remove all collection membership symlinks + def unbookkeep_collections + return unless Dir.exist? item_collections_dir + + existing_collections = Dir.children(item_collections_dir) + existing_collections.each do |coll_pid| + collection_items_dir = collection_member_dir(coll_pid) + next unless Dir.exist? collection_items_dir + + FileUtils.rm(File.join(collection_items_dir, local_part(item.pid))) + end + + members_dir = collection_member_dir(item.pid) + if Dir.exist? members_dir + existing_members = Dir.children(members_dir) + existing_members.each do |item_pid| + item_dir = item_collections_dir(item_pid) + next unless Dir.exist? item_dir + + FileUtils.rm(File.join(item_dir, local_part(item.pid))) + end + end + end + # Clear out the document cache for this item def unpublish + unbookkeep_collections PruneService.new(druid: purl_druid).prune! publish_delete_on_success end @@ -62,6 +116,24 @@ def purl_druid @purl_druid ||= DruidTools::PurlDruid.new item.pid, Settings.stacks.local_document_cache_root end + # Get the collection membership directory for an item + def item_collections_dir(item_pid = nil) + item_druid = if item_pid + DruidTools::PurlDruid.new item_pid, Settings.stacks.local_document_cache_root + else + purl_druid + end + + File.join(item_druid.content_dir, 'is_member_of_collection') + end + + # Get the collection members directory for a collection + def collection_member_dir(collection_pid) + collection_druid = DruidTools::PurlDruid.new collection_pid, Settings.stacks.local_document_cache_root + File.join(collection_druid.content_dir, 'has_member_of_collection') + end + + ## # When publishing a PURL, we notify purl-fetcher of changes. # @@ -77,10 +149,12 @@ def publish_delete_on_success end def purl_services_url - id = item.pid.gsub(/^druid:/, '') - raise 'You have not configured perl-fetcher (Settings.purl_services_url).' unless Settings.purl_services_url - "#{Settings.purl_services_url}/purls/#{id}" + "#{Settings.purl_services_url}/purls/#{local_part(item.pid)}" + end + + def local_part(pid) + Dor::PidUtils.remove_druid_prefix(pid) end end diff --git a/spec/services/publish_metadata_service_spec.rb b/spec/services/publish_metadata_service_spec.rb index 56876e909..ec26256e2 100644 --- a/spec/services/publish_metadata_service_spec.rb +++ b/spec/services/publish_metadata_service_spec.rb @@ -67,12 +67,44 @@ druid1 = DruidTools::Druid.new item.pid, purl_root druid1.mkdir File.open(File.join(druid1.path, 'tmpfile'), 'w') { |f| f.write 'junk' } + expect(service).to receive(:unbookkeep_collections) + service.publish expect(File).not_to exist(druid1.path) # it should now be gone expect(WebMock).to have_requested(:delete, 'example.com/purl/purls/ab123cd4567') end end + let(:release_tags) do + { 'Searchworks' => { 'release' => true }, 'Some_special_place' => { 'release' => true } } + end + + # the individual steps are tested below + context 'a public item' do + before do + allow(ReleaseTags).to receive(:for).and_return(release_tags) + item.rightsMetadata.content = "" + end + + it 'calls the appropriate subfunctions' do + allow(service).to receive(:transfer_metadata) + allow(service).to receive(:bookkeep_collections) + allow(service).to receive(:publish_notify_on_success) + + service.publish + + expect(service).to have_received(:transfer_metadata).with(release_tags) + expect(service).to have_received(:bookkeep_collections) + expect(service).to have_received(:publish_notify_on_success) + end + end + end + + describe '#transfer_metadata' do + before do + allow(OpenURI).to receive(:open_uri).with('https://purl-test.stanford.edu/ab123cd4567.xml').and_return('') + end + context 'copies to the document cache' do let(:mods) do <<-EOXML @@ -102,7 +134,6 @@ expect_any_instance_of(described_class).to receive(:transfer_to_document_store).with(/ ) - service.publish + service.send(:transfer_metadata, release_tags) end end @@ -140,16 +171,34 @@ expect_any_instance_of(described_class).to receive(:transfer_to_document_store).with(/