Skip to content

Commit

Permalink
Merge pull request #84 from performant-software/feature/iiif78_resour…
Browse files Browse the repository at this point in the history
…ce_status

IIIF #78 - Resource Status
  • Loading branch information
dleadbetter authored Jan 15, 2025
2 parents 83bf935 + efd26e8 commit beaa685
Show file tree
Hide file tree
Showing 17 changed files with 464 additions and 21 deletions.
30 changes: 20 additions & 10 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
############################

# Length of JWT expiration in hours
# Recommeded: 24
# Recommended: 24
AUTHENTICATION_EXPIRATION=24

# DB Host name. In docker-compose setup this should be db.
Expand All @@ -23,29 +23,39 @@ DATABASE_PASSWORD=postgres
DATABASE_NAME=dev

# In Docker compose mode, this is the location
# on tehe docker host where DB data should be stored
# on the docker host where DB data should be stored
DB_VOLUME=./tmp/db

# The password to assign the admin login.
# The username will be admin
CANTELOUPE_ADMIN_PASSWORD=mySecret
CANTALOUPE_ADMIN_PASSWORD=mySecret

# Set to true if you want to have an admin dashboard
# on the canteloupe installation
CANTELOUPE_ADMIN_ENABLED=true
# on the Cantaloupe installation
CANTALOUPE_ADMIN_ENABLED=true

# Set to configure the Java opts to pass to the
# Cantalope service
# Cantaloupe service
CANTALOUPE_JAVA_OPTS=-Xmx4g

# The hostname for the canteloupe server
# Set to true if you want to have an admin API
# on the Cantaloupe installation
CANTALOUPE_API_ENABLED=true

# API username for the Cantaloupe instance
CANTALOUPE_API_USERNAME=admin

# API password for the Cantaloupe instance
CANTALOUPE_API_PASSWORD=mySecret

# The hostname for the Cantaloupe server
IIIF_HOST=http://example.com

# The redis url in redis format
REDIS_URL=redis://localhost:6379/1

# Number of concurrent worker threads the IIIF CMS should use
# Recommeded: 5
# Recommended: 5
SIDEKIQ_CONCURRENCY=5

# If you are using an non Amazon S3 compatible object
Expand All @@ -71,11 +81,11 @@ AWS_REGION=us-east-1
AWS_BUCKET_NAME=<your bucket name>

# Set to true to have rails serve static files
# Recommeded: true
# Recommended: true
RAILS_SERVE_STATIC_FILES=true

# Set to true to have rails send logs to STDOUT
# Recommeded: true
# Recommended: true
RAILS_LOG_TO_STDOUT=true

# Rails environment
Expand Down
40 changes: 40 additions & 0 deletions app/controllers/api/resources_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,32 @@ class Api::ResourcesController < Api::BaseController
before_action :validate_resource, unless: -> { current_user.admin? }, only: [:update, :destroy]
before_action :validate_resources, unless: -> { current_user.admin? }, only: :index

def clear_cache
render json: { errors: [I18n.t('errors.resources_controller.clear_cache')] }, status: :unauthorized and return unless can_clear_cache?

resource = Resource.find(params[:id])
key = resource.send(params[:attribute])&.key

service = Cantaloupe::Api.new
response = service.clear_cache(key)

if response.success?
render json: {}, status: :ok
else
error = response['exception'] || response['message'] || response['errors']
render json: { errors: [error] }, status: :bad_request
end
end

def convert
render json: { errors: [I18n.t('errors.resources_controller.convert')] }, status: :unauthorized and return unless current_user.admin?

resource = Resource.find(params[:id])
ConvertImageJob.perform_later(resource.id)

render json: {}, status: :ok
end

protected

def base_query
Expand All @@ -34,6 +60,20 @@ def base_query

private

def can_clear_cache?
# Only admin users can clear the cache
return false unless current_user.admin?

# Only clear the cache if the "attribute" param is present
return false unless params[:attribute].present?

# Only clear the cache if we have the necessary environment variables set
vars = %w(CANTALOUPE_API_USERNAME CANTALOUPE_API_PASSWORD IIIF_HOST)
return false unless vars.all? {|v| ENV[v].present? }

true
end

def set_defineable_params
return unless params[:project_id].present?

Expand Down
13 changes: 13 additions & 0 deletions app/serializers/resources_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,20 @@ class ResourcesSerializer < BaseSerializer

show_attributes(:manifest_url) { |resource| manifest_url(resource) }

show_attributes(:content_info) { |resource| content_info(resource.content) }
show_attributes(:content_converted_info) { |resource| content_info(resource.content_converted) }

def self.manifest_url(resource)
"#{ENV['HOSTNAME']}/public/resources/#{resource.uuid}/manifest"
end

def self.content_info(attachment)
return unless attachment.attached?

{
key: attachment.key,
byte_size: attachment.byte_size,
content_type: attachment.content_type
}
end
end
19 changes: 19 additions & 0 deletions app/services/cantaloupe/api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module Cantaloupe
class Api

def clear_cache(key)
auth = {
username: ENV['CANTALOUPE_API_USERNAME'],
password: ENV['CANTALOUPE_API_PASSWORD']
}

body = {
verb: 'PurgeItemFromCache',
identifier: key
}

HTTParty.post("#{ENV['IIIF_HOST']}/tasks", body: body.to_json, basic_auth: auth)
end

end
end
80 changes: 80 additions & 0 deletions client/src/components/AttachmentDetails.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// @flow

import cx from 'classnames';
import React, { type ComponentType } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, Image, List } from 'semantic-ui-react';
import FileUtils from '../utils/File';
import styles from './AttachmentDetails.module.css';

type Props = {
attachment?: {
byte_size: number,
content_type: string,
key: string,
}
};

const AttachmentDetails: ComponentType<any> = (props: Props) => {
const { t } = useTranslation();

return (
<div
className={styles.attachmentDetails}
>
{ !props.attachment && (
<span>
{ t('AttachmentDetails.messages.noContent') }
</span>
)}
{ props.attachment && (
<List
className={cx(styles.ui, styles.list, styles.horizontal)}
horizontal
>
<List.Item
content={props.attachment?.key}
header={t('AttachmentDetails.labels.key')}
image={(
<Image>
<Icon
circular
name='aws'
size='large'
/>
</Image>
)}
/>
<List.Item
content={FileUtils.getFileSize(props.attachment?.byte_size)}
header={t('AttachmentDetails.labels.fileSize')}
image={(
<Image>
<Icon
circular
name='file alternate'
size='large'
/>
</Image>
)}
/>
<List.Item
content={props.attachment?.content_type}
header={t('AttachmentDetails.labels.type')}
image={(
<Image>
<Icon
circular
name='th large'
size='large'
/>
</Image>
)}
/>
</List>
)}
</div>
);
};

export default AttachmentDetails;
9 changes: 9 additions & 0 deletions client/src/components/AttachmentDetails.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.attachmentDetails {
margin-top: 1.5em;
}

.attachmentDetails > .ui.list.horizontal {
display: flex;
justify-content: space-between;
width: 100%;
}
31 changes: 31 additions & 0 deletions client/src/components/AttachmentStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @flow

import React, { type ComponentType } from 'react';
import { Icon } from 'semantic-ui-react';

type Props = {
className?: string,
value: boolean
};

const AttachmentStatus: ComponentType<any> = (props: Props) => {
if (props.value) {
return (
<Icon
className={props.className}
color='green'
name='check circle'
/>
);
}

return (
<Icon
className={props.className}
color='red'
name='times circle'
/>
);
};

export default AttachmentStatus;
3 changes: 2 additions & 1 deletion client/src/components/SimpleEditPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import type { Translateable } from '../types/Translateable';

type Props = EditContainerProps & Translateable & {
className?: string,
onTabClick?: (tab: string) => void,
stickyMenu?: boolean
};
Expand Down Expand Up @@ -138,7 +139,7 @@ const SimpleEditPage: ComponentType<any> = (props: Props) => {

return (
<Grid
className={styles.simpleEditPage}
className={cx(styles.simpleEditPage, props.className)}
>
<Grid.Row>
<Grid.Column>
Expand Down
25 changes: 25 additions & 0 deletions client/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@
"success": "You've successfully authenticated"
}
},
"AttachmentDetails": {
"labels": {
"fileSize": "File size",
"key": "Key",
"type": "Type"
},
"messages": {
"noContent": "No attached content."
}
},
"Common": {
"buttons": {
"add": "Add",
Expand Down Expand Up @@ -105,11 +115,26 @@
},
"Resource": {
"buttons": {
"clearCache": "Clear cache",
"convert": "Convert",
"exif": "View Info"
},
"labels": {
"attachments": "Attachments",
"content": "Content",
"convertedImage": "Converted PTIF",
"sourceImage": "Source Image",
"uuid": "Unique identifier"
},
"messages": {
"cache": {
"content": "Successfully removed the {{tab}} from IIIF cache.",
"header": "Cache Cleared"
},
"convert": {
"content": "A request to convert the resource to a PTIF has been submitted. Please refresh the page to view the results.",
"header": "Convert Resource"
}
}
},
"ResourceExifModal": {
Expand Down
Loading

0 comments on commit beaa685

Please sign in to comment.