Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
elohanlon committed Feb 20, 2024
1 parent 681501b commit 98c3502
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 2 deletions.
24 changes: 24 additions & 0 deletions app/controllers/image_proxy_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class ImageProxyController < ApplicationController
include ActionController::Live

def raster
remote_url = "#{IMAGE_SERVER_CONFIG[:url]}/iiif/2/#{params[:path]}.#{params[:format]}"
uri = URI(remote_url)
Net::HTTP.start(uri.host, uri.port) do |http|
request = Net::HTTP::Get.new(uri)
request['Authorization'] = "Bearer #{IMAGE_SERVER_CONFIG[:token]}"

http.request request do |remote_response|
response.headers['Content-Length'] = remote_response['Content-Length']
response.headers['Content-Type'] = remote_response['Content-Type']
response.status = :ok

remote_response.read_body do |chunk|
response.stream.write chunk
end
end
ensure
response.stream.close
end
end
end
58 changes: 57 additions & 1 deletion app/frontend/entrypoints/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ console.log('Vite ⚡️ Rails')
// If you want to use .jsx or .tsx, add the extension:
// <%= vite_javascript_tag 'application.jsx' %>

console.log('Visit the guide for more information: ', 'https://vite-ruby.netlify.app/guide/rails')
// console.log('Visit the guide for more information: ', 'https://vite-ruby.netlify.app/guide/rails')

// Example: Load Rails libraries in Vite.
//
Expand All @@ -26,3 +26,59 @@ console.log('Visit the guide for more information: ', 'https://vite-ruby.netlify

// Example: Import a stylesheet in app/frontend/index.css
// import '~/index.css'

const registerImageAuthServiceWorker = async () => {
// console.log('registerServiceWorker called');

// Code below only runs if the browser supports service workers
if ("serviceWorker" in navigator) {
try {
const workerScriptPath = "/image-auth-service-worker.js";

let registration = await navigator.serviceWorker.register(workerScriptPath, {
scope: "/",
});
await navigator.serviceWorker.ready;

// registration.active.postMessage("Test message sent immediately after creation");

// The `if` statement below handles browser hard refreshes, where the service worker doesn't
// actually start running after a hard refresh. This triggers a regular refresh of the page,
// which starts the service worker properly.
if (registration.active && !navigator.serviceWorker.controller) {
// Perform a soft reload to load everything from the SW and get
// a consistent set of resources.
window.location.reload();
}

registration.active.postMessage({
type: 'setImageServerUrl',
url: Hyacinth.imageServerUrl,
});

window.addEventListener('registerAuthToken', (e) => {
console.log("Received registerAuthToken event");
registration.active.postMessage({
type: 'registerAuthToken',
identifier: e.detail.identifier,
token: e.detail.token
});
});
} catch (error) {
console.error(`Service worker registration failed with error: ${error}`);
}
}
else {
console.log("Unable to create a service worker because navigator.serviceWorker returned a falsy value.");
}
};
registerImageAuthServiceWorker();

setTimeout(() => {
window.dispatchEvent(new CustomEvent('registerAuthToken', {
detail: {
identifier: 'test:h18931zcsk',
token: 'public'
}
}));
}, 1000);
4 changes: 3 additions & 1 deletion app/jobs/update_image_service_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ def payload_for_image_service_update_request(asset)
source_uri: image_source_uri_for_digital_object(asset),
featured_region: asset.featured_region,
# Supply pcdm type for MAIN resource (not access / poster)
pcdm_type: asset.pcdm_type
pcdm_type: asset.pcdm_type,
# We need to let the image server know whether or not this is a protected image
has_view_limitation: asset.has_view_limitation_field?
}
}
end
Expand Down
12 changes: 12 additions & 0 deletions app/models/concerns/digital_object/dynamic_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,16 @@ def recursively_generate_csv_style_flattened_dynamic_field_data(df_data, omit_bl
flat_hash
end
end

# Returns true if this DigitalObject has at least one field value
# for a DynamicField::Type::VIEW_LIMITATION field.
def has_view_limitation_field?
view_limitation_field_string_keys = ::DynamicField.where(dynamic_field_type: DynamicField::Type::VIEW_LIMITATION).pluck(:string_key)
view_limitation_field_string_keys.each do |view_limitation_field_string_key|
results = Hyacinth::Utils::HashUtils.find_nested_hashes_that_contain_key(self.dynamic_field_data, view_limitation_field_string_key)
return true if results.present?
end

false
end
end
4 changes: 4 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.

config.log_level = :error

# In the development environment your application's code is reloaded on
# every request. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
Expand Down Expand Up @@ -73,4 +75,6 @@

# Uncomment if you wish to allow Action Cable access from any origin.
# config.action_cable.disable_request_forgery_protection = true

config.hosts << "hyacinth-local.library.columbia.edu:3000"
end
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require 'resque/server'

Rails.application.routes.draw do
get '/image_proxy/iiif/2/*path', to: 'image_proxy#raster', constraints: {all: /.+/}

resources :csv_exports, only: [:index, :create, :show, :destroy] do
member do
get 'download'
Expand Down
79 changes: 79 additions & 0 deletions public/image-auth-service-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
console.log("Image auth service worker started running at: " + new Date().getTime());

let imageServerUrl = null;
const pidsToTokens = new Map();
const iiifUrlWithRequiredAuthRegex = /.+\/iiif\/2\/(standard|limited)\/([^/]+)\/.+/;

async function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

function registerAuthToken(identifier, token) {
// TODO: Also clear out some old tokens (with addedAt time that's far in the past) so that
// the pidsToTokens Map doesn't grow indefinitely.
pidsToTokens.set(
identifier,
{
token: token,
addedAt: Date.now()
}
);
};

async function checkForAuthToken(identifier) {
console.log(`Checking for ${identifier}`);
const start = Date.now();
const checkDelayTimes = [0, 100, 3000]; // [100, 200]
for (const delayTime of checkDelayTimes) {
await sleep(delayTime);
console.log(`Checking for ${identifier} in pidsToTokens Map after ${delayTime} millisecond delay.`);
if (pidsToTokens.has(identifier)) {
return pidsToTokens.get(identifier);
}
}
return null;
}

async function fetchWithAuthorizationHeader(request) {

console.log(`Adding header to request: ${request.url}`);
const identifier = request.url.match(iiifUrlWithRequiredAuthRegex)[2];

// Check if there is a token available for this identifier
const entry = await checkForAuthToken(identifier);
const headers = new Headers(request.headers);
if (entry) {
console.log(`Making request with token ${entry.token}`);
headers.set('Authorization', `Bearer ${entry ? entry.token : null}`);
}

const newRequest = new Request(request, {
mode: 'cors',
credentials: 'omit',
headers: headers
});

return fetch(newRequest);
}

self.addEventListener('fetch', function (event) {
const url = event.request.url;
// console.log(`imageServerUrl: ${imageServerUrl}`);
// console.log(`Intercepting fetch request for: ${url}`);
if (url.startsWith(imageServerUrl) && url.match(iiifUrlWithRequiredAuthRegex)) {
event.respondWith(fetchWithAuthorizationHeader(event.request));
}
});

self.addEventListener("message", (event) => {
const { data } = event;
console.log('Service worker received message: ', data);

if (data.type == 'registerAuthToken') {
registerAuthToken(data.identifier, data.token);
}

if (data.type == 'setImageServerUrl') {
imageServerUrl = data.url;
}
});

0 comments on commit 98c3502

Please sign in to comment.