Skip to content

Commit

Permalink
Auxiliary & Access file download
Browse files Browse the repository at this point in the history
  • Loading branch information
kdid committed Apr 30, 2024
1 parent 8c0c583 commit d0a11bf
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 9 deletions.
21 changes: 21 additions & 0 deletions node/src/api/response/iiif/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ const {
buildAnnotationBody,
buildImageResourceId,
buildImageService,
isAltFormat,
isAudioVideo,
isPDF,
buildSupplementingAnnotation,
} = require("./presentation-api/items");
const { metadataLabelFields } = require("./presentation-api/metadata");
Expand Down Expand Up @@ -108,6 +110,24 @@ function transform(response) {
},
});

/** Add rendering */
let renderings = [];
source.file_sets
.filter((fileSet) => fileSet.role === "Auxiliary")
.filter((fileSet) => isPDF(fileSet.mime_type))
.forEach((fileSet) => {
const rendering = {
id: fileSet.download_url || null,
type: "Text",
label: {
en: [fileSet.label || "Download PDF"],
},
format: "application/pdf",
};
renderings.push(rendering);
});
manifest.setRendering(renderings);

/** Add rights using rights statement */
source.rights_statement?.id &&
manifest.setRights(source.rights_statement.id);
Expand Down Expand Up @@ -203,6 +223,7 @@ function transform(response) {

source.file_sets
.filter((fileSet) => fileSet.role === "Auxiliary")
.filter((fileSet) => !isAltFormat(fileSet.mime_type))
.forEach((fileSet, index) => {
buildCanvasFromFileSet(fileSet, index, true);
});
Expand Down
15 changes: 15 additions & 0 deletions node/src/api/response/iiif/presentation-api/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ function buildSupplementingAnnotation({ canvasId, fileSet }) {
};
}

function isAltFormat(mimeType) {
const acceptedTypes = [
"application/pdf",
"application/zip",
"application/zip-compressed",
];
return acceptedTypes.includes(mimeType);
}

function isAudioVideo(type) {
return ["Audio", "Video", "Sound"].includes(type);
}
Expand All @@ -65,13 +74,19 @@ function isImage(workType) {
return workType === "Image";
}

function isPDF(mimeType) {
return mimeType === "application/pdf";
}

module.exports = {
annotationType,
buildAnnotationBody,
buildAnnotationBodyId,
buildImageResourceId,
buildImageService,
buildSupplementingAnnotation,
isAltFormat,
isAudioVideo,
isImage,
isPDF,
};
144 changes: 135 additions & 9 deletions node/src/handlers/get-file-set-download.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ const { SFNClient, StartExecutionCommand } = require("@aws-sdk/client-sfn");
const { wrap } = require("./middleware");
const { getFileSet } = require("../api/opensearch");
const { videoTranscodeSettings } = require("./transcode-templates");
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3");
const { apiTokenName } = require("../environment");
const ApiToken = require("../api/api-token");
const axios = require("axios").default;
const cookie = require("cookie");

const opensearchResponse = require("../api/response/opensearch");
const path = require("path");

/**
* Handler for download file set endpoint (currently only handles VIDEO)
* Handler for download file set endpoint
*/
exports.handler = wrap(async (event) => {
const id = event.pathParameters.id;
Expand All @@ -16,30 +22,52 @@ exports.handler = wrap(async (event) => {
return invalidRequest(400, "Query string must include email address");
}

if (!event.userToken.isSuperUser()) {
return invalidRequest(401, "Unauthorized");
}
// if (!event.userToken.isSuperUser()) {
// return invalidRequest(401, "Unauthorized");
// }
const esResponse = await getFileSet(id, {
allowPrivate: true,
allowUnpublished: true,
});

if (esResponse.statusCode == "200") {
const doc = JSON.parse(esResponse.body);
if (downloadAvailable(doc)) {
return await processDownload(doc, email);
if (isVideoDownload(doc)) {
return await processAVDownload(doc, email);
} else if (isImageDownload(doc)) {
return await IIIFImageRequest(doc);
} else if (isAltFileDownload(doc)) {
const url = await getDownloadLink(doc);
return {
statusCode: 302,
headers: { "Location": url },
};
} else {
return invalidRequest(
405,
"Download only allowed for role: Access, work_type: Video, with a valid streaming_url"
"Download not allowed for role + work_type"
);
}
} else {
return await opensearchResponse.transform(esResponse);
}
});

function downloadAvailable(doc) {
function isAltFileDownload(doc) {
const acceptedTypes = [
"application/pdf",
"application/zip",
"application/zip-compressed",
];
return (
doc.found &&
doc._source.role === "Auxiliary" &&
doc._source.mime_type != null &&
acceptedTypes.includes(doc._source.mime_type)
);
}

function isVideoDownload(doc) {
// Note - audio is not currently implemented due to an issue with AWS
// & MediaConvert and our .m3u8 files
return (
Expand All @@ -51,7 +79,105 @@ function downloadAvailable(doc) {
);
}

async function processDownload(doc, email) {
function isImageDownload(doc) {
return (
doc.found &&
["Access", "Auxiliary"].includes(doc._source.role) &&
doc._source.mime_type != null &&
["image"].includes(doc._source.mime_type.split("/")[0])
);
}

function derivativeKey(doc) {
const id = doc._id;
let prefix =
id.slice(0, 2) +
"/" +
id.slice(2, 4) +
"/" +
id.slice(4, 6) +
"/" +
id.slice(6, 8);
return "derivatives/" + prefix + "/" + id;
}

function extensionFromMimeType(mimeType) {
const parts = mimeType.split("/");
if (parts.length > 1) {
return parts[1];
}
return "";
}

async function getDownloadLink(doc) {
const clientParams = {};
const bucket = process.env.PYRAMID_BUCKET;
const key = derivativeKey(doc);

const getObjectParams = {
Bucket: bucket,
Key: key,
ResponseContentDisposition: `attachment; filename=${
doc._source.label
}.${extensionFromMimeType(doc._source.mime_type)}`,
};

const client = new S3Client(clientParams);
const command = new GetObjectCommand(getObjectParams);
const url = await getSignedUrl(client, command, { expiresIn: 3600 * 24 * 3 }); // 3 days
return url;
}

const getAxiosResponse = (url, config)=> {
return new Promise((resolve) => {
axios
.get(url, config)
.then((response) => resolve(response))
.catch((error) => resolve(error.response));
});
}

const IIIFImageRequest = async (doc) => {
const dimensions = "/full/max/0/default.jpg";
const iiifImageBaseUrl = doc._source.representative_image_url;
const url = `${iiifImageBaseUrl}${dimensions}`;
console.log(url)
const { status, headers, data } = await getAxiosResponse(url, {
headers: {
cookie: cookie.serialize(
apiTokenName(),
new ApiToken().superUser().sign(),
{
domain: "library.northwestern.edu",
path: "/",
secure: true,
}
),
},
responseType: "arraybuffer",
});

console.log("data", data.toString());

if (status != 200) {
return {
statusCode: status,
body: data.toString(),
headers: headers,
};
}

return {
statusCode: status,
isBase64Encoded: true,
body: data.toString("base64"),
headers: {
"content-type": headers["content-type"],
},
};
}

async function processAVDownload(doc, email) {
const stepFunctionConfig = process.env.STEP_FUNCTION_ENDPOINT
? { endpoint: process.env.STEP_FUNCTION_ENDPOINT }
: {};
Expand Down
4 changes: 4 additions & 0 deletions template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ Parameters:
NussoBaseUrl:
Type: String
Description: Auth server URL
PyramidBucket:
Type: String
Description: Meadow pyramid bucket
ReadingRoomIPs:
Type: String
Description: Comma-delimited list of IP addresses to serve private resources to
Expand Down Expand Up @@ -340,6 +343,7 @@ Resources:
MEDIA_CONVERT_ENDPOINT: !Ref MediaConvertEndpoint
MEDIA_CONVERT_JOB_QUEUE_ARN: !Ref MediaConvertJobQueueArn
MEDIA_CONVERT_ROLE_ARN: !Ref MediaConvertRoleArn
PYRAMID_BUCKET: !Ref PyramidBucket
REPOSITORY_EMAIL: !Ref RepositoryEmail
START_TRANSCODE_FUNCTION: !GetAtt startTranscodeFunction.Arn
TRANSCODE_STATUS_FUNCTION: !GetAtt transcodeStatusFunction.Arn
Expand Down

0 comments on commit d0a11bf

Please sign in to comment.