queued downloads and stream not buffer of downloads
This commit is contained in:
@@ -45,3 +45,5 @@ public/swagger.json
|
|||||||
certificates/cert-key.pem
|
certificates/cert-key.pem
|
||||||
certificates/cert.pem
|
certificates/cert.pem
|
||||||
gitclean.sh
|
gitclean.sh
|
||||||
|
pages/admin/logcheck.js
|
||||||
|
pages/admin/logchecker.js
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
import { setCurrentPage } from "../../store/currentView/action";
|
import { setCurrentPage } from "../../store/currentView/action";
|
||||||
|
|
||||||
import { setDocumentDetails } from "../../store/searchOutput/action";
|
import { setDocumentDetails } from "../../store/searchOutput/action";
|
||||||
|
import DocumentLink from "../utils/downloads";
|
||||||
|
import { useDownloadQueue } from "../utils/downloadmanager";
|
||||||
|
|
||||||
const DocumentDetails = (props) => {
|
const DocumentDetails = (props) => {
|
||||||
let { t, lang } = useTranslation();
|
let { t, lang } = useTranslation();
|
||||||
@@ -37,7 +39,7 @@ const DocumentDetails = (props) => {
|
|||||||
const { locale } = router;
|
const { locale } = router;
|
||||||
|
|
||||||
var documentDetailsArr = documentDetailsObj || [];
|
var documentDetailsArr = documentDetailsObj || [];
|
||||||
|
const { startDownload, getStatus } = useDownloadQueue(3);
|
||||||
const [selectedOption, setSelectedOption] = useState(10);
|
const [selectedOption, setSelectedOption] = useState(10);
|
||||||
const [documentTypes, setDocumentTypes] = useState([{}]);
|
const [documentTypes, setDocumentTypes] = useState([{}]);
|
||||||
const [selectedDocumentType, setSelectedDocumentType] = useState("all");
|
const [selectedDocumentType, setSelectedDocumentType] = useState("all");
|
||||||
@@ -764,41 +766,63 @@ const DocumentDetails = (props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!ShowDocLinks ? (
|
{!ShowDocLinks ? (
|
||||||
<Link
|
<>
|
||||||
scroll={false}
|
<DocumentLink
|
||||||
href={
|
key={
|
||||||
detailsObj.pinswg_hashlink
|
detailsObj.pinswg_documentid
|
||||||
}
|
}
|
||||||
key={key}
|
id={
|
||||||
onClick={() => {
|
detailsObj.pinswg_documentid
|
||||||
toggleDownLoadLink(
|
}
|
||||||
key
|
detailsObj={
|
||||||
),
|
detailsObj
|
||||||
sendGAEvent(
|
}
|
||||||
"event",
|
caseReference={
|
||||||
"DownloadedFile",
|
caseReference
|
||||||
{
|
}
|
||||||
caseReference:
|
startDownload={
|
||||||
props.caseReference,
|
startDownload
|
||||||
filename:
|
}
|
||||||
detailsObj.pinswg_name,
|
getStatus={
|
||||||
}
|
getStatus
|
||||||
);
|
}
|
||||||
}}
|
/>
|
||||||
>
|
{/* <Link
|
||||||
<span className="results-visually-hidden">
|
scroll={false}
|
||||||
{t(
|
href={
|
||||||
"case:documents-name-label"
|
detailsObj.pinswg_hashlink
|
||||||
)}
|
}
|
||||||
:
|
key={key}
|
||||||
</span>
|
onClick={() => {
|
||||||
{_.has(
|
toggleDownLoadLink(
|
||||||
detailsObj,
|
key
|
||||||
"pinswg_name"
|
),
|
||||||
)
|
sendGAEvent(
|
||||||
? detailsObj.pinswg_name
|
"event",
|
||||||
: ""}
|
"DownloadedFile",
|
||||||
</Link>
|
{
|
||||||
|
caseReference:
|
||||||
|
props.caseReference,
|
||||||
|
filename:
|
||||||
|
detailsObj.pinswg_name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="results-visually-hidden">
|
||||||
|
{t(
|
||||||
|
"case:documents-name-label"
|
||||||
|
)}
|
||||||
|
:
|
||||||
|
</span>
|
||||||
|
{_.has(
|
||||||
|
detailsObj,
|
||||||
|
"pinswg_name"
|
||||||
|
)
|
||||||
|
? detailsObj.pinswg_name
|
||||||
|
: ""}
|
||||||
|
</Link> */}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span className="results-visually-hidden">
|
<span className="results-visually-hidden">
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { useState, useCallback } from "react";
|
||||||
|
|
||||||
|
export function useDownloadQueue(maxConcurrent = 3) {
|
||||||
|
const [queue, setQueue] = useState([]); // queued tasks
|
||||||
|
const [activeCount, setActiveCount] = useState(0);
|
||||||
|
const [statuses, setStatuses] = useState({}); // { id: "idle"|"queued"|"downloading"|"done"|"failed" }
|
||||||
|
|
||||||
|
const scheduleNext = useCallback(() => {
|
||||||
|
setQueue((queueCurr) => {
|
||||||
|
if (activeCount >= maxConcurrent || queueCurr.length === 0)
|
||||||
|
return queueCurr;
|
||||||
|
|
||||||
|
const [task, ...rest] = queueCurr;
|
||||||
|
|
||||||
|
setStatuses((s) => ({ ...s, [task.id]: "downloading" }));
|
||||||
|
setActiveCount((c) => c + 1);
|
||||||
|
|
||||||
|
// Run the download fully in async IIFE
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await task.downloadFn(); // triggers fetch + blob + <a> click
|
||||||
|
setStatuses((s) => ({ ...s, [task.id]: "done" }));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setStatuses((s) => ({ ...s, [task.id]: "failed" }));
|
||||||
|
} finally {
|
||||||
|
setActiveCount((c) => c - 1);
|
||||||
|
scheduleNext(); // promote next in queue
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
|
}, [activeCount, maxConcurrent]);
|
||||||
|
|
||||||
|
const startDownload = useCallback(
|
||||||
|
(id, downloadFn) => {
|
||||||
|
setStatuses((s) => ({ ...s, [id]: "queued" }));
|
||||||
|
setQueue((curr) => [...curr, { id, downloadFn }]);
|
||||||
|
scheduleNext();
|
||||||
|
},
|
||||||
|
[scheduleNext]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getStatus = useCallback((id) => statuses[id] || "idle", [statuses]);
|
||||||
|
|
||||||
|
return { startDownload, getStatus };
|
||||||
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
// // import { useState } from "react";
|
||||||
|
|
||||||
|
// // export default function DocumentLink({ detailsObj, caseReference }) {
|
||||||
|
// // const [downloadStatus, setDownloadStatus] = useState("");
|
||||||
|
// // const [progress, setProgress] = useState(null);
|
||||||
|
|
||||||
|
// // const handleDownload = async () => {
|
||||||
|
// // setDownloadStatus("Downloading");
|
||||||
|
// // setProgress(0);
|
||||||
|
|
||||||
|
// // try {
|
||||||
|
// // const res = await fetch(detailsObj.pinswg_hashlink);
|
||||||
|
// // if (!res.ok) throw new Error("Download failed");
|
||||||
|
|
||||||
|
// // const contentDisposition = res.headers.get("content-disposition");
|
||||||
|
// // const filename = contentDisposition
|
||||||
|
// // ? contentDisposition.split("filename=")[1]
|
||||||
|
// // : detailsObj.pinswg_name || "document";
|
||||||
|
|
||||||
|
// // const contentLength = res.headers.get("content-length");
|
||||||
|
// // const totalBytes = contentLength
|
||||||
|
// // ? parseInt(contentLength, 10)
|
||||||
|
// // : null;
|
||||||
|
|
||||||
|
// // const reader = res.body.getReader();
|
||||||
|
// // let receivedBytes = 0;
|
||||||
|
// // const chunks = [];
|
||||||
|
|
||||||
|
// // while (true) {
|
||||||
|
// // const { done, value } = await reader.read();
|
||||||
|
// // if (done) break;
|
||||||
|
|
||||||
|
// // chunks.push(value);
|
||||||
|
// // receivedBytes += value.length;
|
||||||
|
|
||||||
|
// // if (totalBytes) {
|
||||||
|
// // const percent = Math.round(
|
||||||
|
// // (receivedBytes / totalBytes) * 100
|
||||||
|
// // );
|
||||||
|
// // setProgress(percent);
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // // Combine chunks into one blob
|
||||||
|
// // const blob = new Blob(chunks);
|
||||||
|
// // const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// // const link = document.createElement("a");
|
||||||
|
// // link.href = url;
|
||||||
|
// // link.download = filename;
|
||||||
|
// // document.body.appendChild(link);
|
||||||
|
// // link.click();
|
||||||
|
// // document.body.removeChild(link);
|
||||||
|
|
||||||
|
// // setDownloadStatus("Download complete!");
|
||||||
|
// // setProgress(100);
|
||||||
|
|
||||||
|
// // // Fire GA event
|
||||||
|
// // window.gtag?.("event", "DownloadedFile", {
|
||||||
|
// // caseReference,
|
||||||
|
// // filename,
|
||||||
|
// // });
|
||||||
|
// // } catch (err) {
|
||||||
|
// // console.error(err);
|
||||||
|
// // setDownloadStatus("Download failed");
|
||||||
|
// // setProgress(null);
|
||||||
|
// // }
|
||||||
|
// // };
|
||||||
|
|
||||||
|
// // return (
|
||||||
|
// // <>
|
||||||
|
// // {detailsObj.pinswg_publishtoweb ? (
|
||||||
|
// // <button className="documentLink" onClick={handleDownload}>
|
||||||
|
// // <span className="results-visually-hidden">
|
||||||
|
// // Document name:
|
||||||
|
// // </span>
|
||||||
|
// // {detailsObj.pinswg_name || ""}
|
||||||
|
// // </button>
|
||||||
|
// // ) : (
|
||||||
|
// // <>
|
||||||
|
// // <span className="results-visually-hidden">
|
||||||
|
// // Document name:
|
||||||
|
// // </span>
|
||||||
|
// // {detailsObj.pinswg_name || ""}
|
||||||
|
// // </>
|
||||||
|
// // )}
|
||||||
|
|
||||||
|
// // {downloadStatus && (
|
||||||
|
// // <p
|
||||||
|
// // className={
|
||||||
|
// // downloadStatus == "Downloading"
|
||||||
|
// // ? "govuk-!-font-size-14 govuk-!-margin-left-0 textloading"
|
||||||
|
// // : "govuk-!-font-size-14 govuk-!-margin-left-0 "
|
||||||
|
// // }
|
||||||
|
// // >
|
||||||
|
// // {downloadStatus}
|
||||||
|
// // {/* {progress !== null && ` (${progress}%)`} */}
|
||||||
|
// // </p>
|
||||||
|
// // )}
|
||||||
|
// // </>
|
||||||
|
// // );
|
||||||
|
// // }
|
||||||
|
// import { useState } from "react";
|
||||||
|
|
||||||
|
// export default function DocumentLink({
|
||||||
|
// id,
|
||||||
|
// detailsObj,
|
||||||
|
// caseReference,
|
||||||
|
// startDownload,
|
||||||
|
// getStatus,
|
||||||
|
// }) {
|
||||||
|
// const [downloadStatus, setDownloadStatus] = useState("");
|
||||||
|
// const [progress, setProgress] = useState(null);
|
||||||
|
|
||||||
|
// const downloadFile = async () => {
|
||||||
|
// setDownloadStatus("Downloading");
|
||||||
|
// setProgress(0);
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const res = await fetch(detailsObj.pinswg_hashlink);
|
||||||
|
// if (!res.ok) throw new Error("Download failed");
|
||||||
|
|
||||||
|
// const contentDisposition = res.headers.get("content-disposition");
|
||||||
|
// const filename = contentDisposition
|
||||||
|
// ? contentDisposition.split("filename=")[1]
|
||||||
|
// : detailsObj.pinswg_name || "document";
|
||||||
|
|
||||||
|
// const contentLength = res.headers.get("content-length");
|
||||||
|
// const totalBytes = contentLength
|
||||||
|
// ? parseInt(contentLength, 10)
|
||||||
|
// : null;
|
||||||
|
|
||||||
|
// const reader = res.body.getReader();
|
||||||
|
// let receivedBytes = 0;
|
||||||
|
// const chunks = [];
|
||||||
|
|
||||||
|
// while (true) {
|
||||||
|
// const { done, value } = await reader.read();
|
||||||
|
// if (done) break;
|
||||||
|
|
||||||
|
// chunks.push(value);
|
||||||
|
// receivedBytes += value.length;
|
||||||
|
|
||||||
|
// if (totalBytes) {
|
||||||
|
// const percent = Math.round(
|
||||||
|
// (receivedBytes / totalBytes) * 100
|
||||||
|
// );
|
||||||
|
// setProgress(percent);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const blob = new Blob(chunks);
|
||||||
|
// const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// const link = document.createElement("a");
|
||||||
|
// link.href = url;
|
||||||
|
// link.download = filename;
|
||||||
|
// document.body.appendChild(link);
|
||||||
|
// link.click();
|
||||||
|
// document.body.removeChild(link);
|
||||||
|
|
||||||
|
// setDownloadStatus("Download complete!");
|
||||||
|
// setProgress(100);
|
||||||
|
|
||||||
|
// window.gtag?.("event", "DownloadedFile", {
|
||||||
|
// caseReference,
|
||||||
|
// filename,
|
||||||
|
// });
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error(err);
|
||||||
|
// setDownloadStatus("Download failed");
|
||||||
|
// setProgress(null);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleClick = () => {
|
||||||
|
// if (getStatus(id) !== "idle") return; // don't double enqueue
|
||||||
|
// startDownload(id, downloadFile);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const status = getStatus(id);
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <>
|
||||||
|
// {detailsObj.pinswg_publishtoweb ? (
|
||||||
|
// <button className="documentLink" onClick={handleClick}>
|
||||||
|
// <span className="results-visually-hidden">
|
||||||
|
// Document name:
|
||||||
|
// </span>
|
||||||
|
// {detailsObj.pinswg_name || ""}
|
||||||
|
// </button>
|
||||||
|
// ) : (
|
||||||
|
// <>
|
||||||
|
// <span className="results-visually-hidden">
|
||||||
|
// Document name:
|
||||||
|
// </span>
|
||||||
|
// {detailsObj.pinswg_name || ""}
|
||||||
|
// </>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// {status !== "idle" && (
|
||||||
|
// <p
|
||||||
|
// className={
|
||||||
|
// status === "downloading"
|
||||||
|
// ? "govuk-!-font-size-14 govuk-!-margin-left-0 textloading"
|
||||||
|
// : "govuk-!-font-size-14 govuk-!-margin-left-0"
|
||||||
|
// }
|
||||||
|
// >
|
||||||
|
// {status === "downloading" && progress !== null
|
||||||
|
// ? `Downloading (${progress}%)`
|
||||||
|
// : status === "queued"
|
||||||
|
// ? "Queued"
|
||||||
|
// : status === "done"
|
||||||
|
// ? "Download complete!"
|
||||||
|
// : status === "failed"
|
||||||
|
// ? "Download failed"
|
||||||
|
// : ""}
|
||||||
|
// </p>
|
||||||
|
// )}
|
||||||
|
// </>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function DocumentLink({
|
||||||
|
id,
|
||||||
|
detailsObj,
|
||||||
|
caseReference,
|
||||||
|
startDownload,
|
||||||
|
getStatus,
|
||||||
|
}) {
|
||||||
|
const [progress, setProgress] = useState(null);
|
||||||
|
|
||||||
|
const downloadFile = async () => {
|
||||||
|
setProgress(0);
|
||||||
|
|
||||||
|
const res = await fetch(detailsObj.pinswg_hashlink);
|
||||||
|
if (!res.ok) throw new Error("Download failed");
|
||||||
|
|
||||||
|
const contentDisposition = res.headers.get("content-disposition");
|
||||||
|
const filename = contentDisposition
|
||||||
|
? contentDisposition.split("filename=")[1]
|
||||||
|
: detailsObj.pinswg_name || "document";
|
||||||
|
|
||||||
|
const contentLength = res.headers.get("content-length");
|
||||||
|
const totalBytes = contentLength ? parseInt(contentLength, 10) : null;
|
||||||
|
|
||||||
|
const reader = res.body.getReader();
|
||||||
|
let receivedBytes = 0;
|
||||||
|
const chunks = [];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
chunks.push(value);
|
||||||
|
receivedBytes += value.length;
|
||||||
|
|
||||||
|
if (totalBytes) {
|
||||||
|
const percent = Math.round((receivedBytes / totalBytes) * 100);
|
||||||
|
setProgress(percent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob(chunks);
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
setProgress(100);
|
||||||
|
window.gtag?.("event", "DownloadedFile", { caseReference, filename });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (getStatus(id) !== "idle") return; // avoid double enqueue
|
||||||
|
startDownload(id, downloadFile);
|
||||||
|
};
|
||||||
|
|
||||||
|
const status = getStatus(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{detailsObj.pinswg_publishtoweb ? (
|
||||||
|
<button className="documentLink" onClick={handleClick}>
|
||||||
|
<span className="results-visually-hidden">
|
||||||
|
Document name:
|
||||||
|
</span>
|
||||||
|
{detailsObj.pinswg_name || ""}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span className="results-visually-hidden">
|
||||||
|
{detailsObj.pinswg_name || ""}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status !== "idle" && (
|
||||||
|
<p
|
||||||
|
className={
|
||||||
|
status === "downloading"
|
||||||
|
? "govuk-!-font-size-14 govuk-!-margin-left-0 textloading"
|
||||||
|
: "govuk-!-font-size-14 govuk-!-margin-left-0"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{status === "downloading" && progress !== null
|
||||||
|
? `Downloading `
|
||||||
|
: status === "queued"
|
||||||
|
? "Queued"
|
||||||
|
: status === "done"
|
||||||
|
? "Download complete!"
|
||||||
|
: status === "failed"
|
||||||
|
? "Download failed"
|
||||||
|
: ""}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,110 +1,203 @@
|
|||||||
/**
|
// /**
|
||||||
* @swagger
|
// * @swagger
|
||||||
* /api/documents/download/{id}:
|
// * /api/documents/download/{id}:
|
||||||
* get:
|
// * get:
|
||||||
* tags:
|
// * tags:
|
||||||
* - Documents
|
// * - Documents
|
||||||
* description: Get document
|
// * description: Get document
|
||||||
* parameters:
|
// * parameters:
|
||||||
* - name: id
|
// * - name: id
|
||||||
* in: path
|
// * in: path
|
||||||
* description: iShare ID
|
// * description: iShare ID
|
||||||
* type: string
|
// * type: string
|
||||||
* required: true
|
// * required: true
|
||||||
* default: A41567436
|
// * default: A41567436
|
||||||
* - name: hash
|
// * - name: hash
|
||||||
* in: query
|
// * in: query
|
||||||
* description: Hash string
|
// * description: Hash string
|
||||||
* default: e0a1c9cd2817826a25bca67e60b807eae747cbed769e3ec42cf997541c32be71
|
// * default: e0a1c9cd2817826a25bca67e60b807eae747cbed769e3ec42cf997541c32be71
|
||||||
* responses:
|
// * responses:
|
||||||
* 200:
|
// * 200:
|
||||||
* description: Success
|
// * description: Success
|
||||||
*/
|
// */
|
||||||
|
|
||||||
|
// import axios from "axios";
|
||||||
|
// import CryptoJS from "crypto-js";
|
||||||
|
// import { getToken, consoleLogger } from "../../../../actions";
|
||||||
|
|
||||||
|
// const WORDKEY = process.env.HASHKEY;
|
||||||
|
// const accessTokenEndpoint = process.env.ACCESS_TOKEN_ENDPOINT;
|
||||||
|
// const tenantId = process.env.TENANT;
|
||||||
|
|
||||||
|
// const WEBAPI_URL =
|
||||||
|
// process.env.RELAY_ROOT ||
|
||||||
|
// "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
|
||||||
|
|
||||||
|
// const hashAPIPath = (queryPath) => {
|
||||||
|
// var hashlink = CryptoJS.HmacSHA256(
|
||||||
|
// queryPath,
|
||||||
|
// CryptoJS.enc.Hex.parse(WORDKEY)
|
||||||
|
// );
|
||||||
|
// hashlink = hashlink.toString(CryptoJS.enc.Hex);
|
||||||
|
|
||||||
|
// //return "&hash=" + hashlink;
|
||||||
|
|
||||||
|
// return (queryPath.indexOf("?") > -1 ? "&hash=" : "?hash=") + hashlink;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const azureHeadersPaged = (access_token) => {
|
||||||
|
// return {
|
||||||
|
// headers: {
|
||||||
|
// "OData-MaxVersion": "4.0",
|
||||||
|
// "OData-Version": "4.0",
|
||||||
|
// "Accept": "application/json;odata.metadata=none",
|
||||||
|
// "Prefer":
|
||||||
|
// 'odata.include-annotations="*",return=representation, odata.maxpagesize=10',
|
||||||
|
// "Content-Type": "application/json",
|
||||||
|
// "Authorization": "Bearer " + access_token,
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export default async function ApiProxy(req, res) {
|
||||||
|
// var token = await getToken();
|
||||||
|
|
||||||
|
// var docRef = req.query;
|
||||||
|
|
||||||
|
// var queryUrl = "documents/download/" + docRef.id + "?hash=" + docRef.hash;
|
||||||
|
|
||||||
|
// const configDocument = (access_token) => {
|
||||||
|
// return {
|
||||||
|
// headers: {
|
||||||
|
// "OData-MaxVersion": "4.0",
|
||||||
|
// "OData-Version": "4.0",
|
||||||
|
// "Accept": "application/json",
|
||||||
|
// "Prefer": 'odata.include-annotations="*",return=representation',
|
||||||
|
// "Content-Type": "application/json",
|
||||||
|
// "Authorization": "Bearer " + access_token,
|
||||||
|
// },
|
||||||
|
// responseType: "arraybuffer",
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return axios
|
||||||
|
// .get(
|
||||||
|
// WEBAPI_URL + queryUrl, // + hashAPIPath(queryUrl),
|
||||||
|
// configDocument(token.access_token)
|
||||||
|
// )
|
||||||
|
// .then((response) => {
|
||||||
|
// const bytes = response.data.byteLength;
|
||||||
|
// console.log(bytes);
|
||||||
|
|
||||||
|
// res.setHeader(
|
||||||
|
// "content-disposition",
|
||||||
|
// "attachment; filename=" +
|
||||||
|
// response.headers["content-disposition"].split(
|
||||||
|
// "filename="
|
||||||
|
// )[1]
|
||||||
|
// );
|
||||||
|
// return res.status(200).send(response.data);
|
||||||
|
// })
|
||||||
|
// .then(() => {
|
||||||
|
// console.log(docRef.id + "has downloaded");
|
||||||
|
|
||||||
|
// return "downloadComplete";
|
||||||
|
// })
|
||||||
|
// .catch((error) => {
|
||||||
|
// consoleLogger(error);
|
||||||
|
// res.redirect("/filenotavailable");
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import CryptoJS from "crypto-js";
|
|
||||||
import { getToken, consoleLogger } from "../../../../actions";
|
import { getToken, consoleLogger } from "../../../../actions";
|
||||||
|
|
||||||
const WORDKEY = process.env.HASHKEY;
|
|
||||||
const accessTokenEndpoint = process.env.ACCESS_TOKEN_ENDPOINT;
|
|
||||||
const tenantId = process.env.TENANT;
|
|
||||||
|
|
||||||
const WEBAPI_URL =
|
const WEBAPI_URL =
|
||||||
process.env.RELAY_ROOT ||
|
process.env.RELAY_ROOT ||
|
||||||
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
|
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
|
||||||
|
|
||||||
const hashAPIPath = (queryPath) => {
|
// Retry utility with logging
|
||||||
var hashlink = CryptoJS.HmacSHA256(
|
async function retry(fn, retries = 3, delay = 1000) {
|
||||||
queryPath,
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||||
CryptoJS.enc.Hex.parse(WORDKEY)
|
try {
|
||||||
);
|
return { response: await fn(), attempts: attempt };
|
||||||
hashlink = hashlink.toString(CryptoJS.enc.Hex);
|
} catch (err) {
|
||||||
|
if (attempt === retries) throw err;
|
||||||
//return "&hash=" + hashlink;
|
console.log(`Retry ${attempt} failed, retrying in ${delay}ms...`);
|
||||||
|
await new Promise((res) => setTimeout(res, delay));
|
||||||
return (queryPath.indexOf("?") > -1 ? "&hash=" : "?hash=") + hashlink;
|
delay *= 2; // exponential backoff
|
||||||
};
|
}
|
||||||
|
}
|
||||||
const azureHeadersPaged = (access_token) => {
|
|
||||||
return {
|
|
||||||
headers: {
|
|
||||||
"OData-MaxVersion": "4.0",
|
|
||||||
"OData-Version": "4.0",
|
|
||||||
"Accept": "application/json;odata.metadata=none",
|
|
||||||
"Prefer":
|
|
||||||
'odata.include-annotations="*",return=representation, odata.maxpagesize=10',
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": "Bearer " + access_token,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function ApiProxy(req, res) {
|
|
||||||
var token = await getToken();
|
|
||||||
|
|
||||||
var docRef = req.query;
|
|
||||||
|
|
||||||
var queryUrl = "documents/download/" + docRef.id + "?hash=" + docRef.hash;
|
|
||||||
|
|
||||||
const configDocument = (access_token) => {
|
|
||||||
return {
|
|
||||||
headers: {
|
|
||||||
"OData-MaxVersion": "4.0",
|
|
||||||
"OData-Version": "4.0",
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Prefer": 'odata.include-annotations="*",return=representation',
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": "Bearer " + access_token,
|
|
||||||
},
|
|
||||||
responseType: "arraybuffer",
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return axios
|
|
||||||
.get(
|
|
||||||
WEBAPI_URL + queryUrl, // + hashAPIPath(queryUrl),
|
|
||||||
configDocument(token.access_token)
|
|
||||||
)
|
|
||||||
.then((response) => {
|
|
||||||
const bytes = response.data.byteLength;
|
|
||||||
console.log(bytes);
|
|
||||||
|
|
||||||
res.setHeader(
|
|
||||||
"content-disposition",
|
|
||||||
"attachment; filename=" +
|
|
||||||
response.headers["content-disposition"].split(
|
|
||||||
"filename="
|
|
||||||
)[1]
|
|
||||||
);
|
|
||||||
return res.status(200).send(response.data);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
console.log(docRef.id + "has downloaded");
|
|
||||||
|
|
||||||
return "downloadComplete";
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
consoleLogger(error);
|
|
||||||
res.redirect("/filenotavailable");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ApiProxy = async (req, res) => {
|
||||||
|
const docRef = req.query;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = await getToken();
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const fetchStream = async () => {
|
||||||
|
const queryUrl =
|
||||||
|
"documents/download/" + docRef.id + "?hash=" + docRef.hash;
|
||||||
|
|
||||||
|
return axios.get(WEBAPI_URL + queryUrl, {
|
||||||
|
headers: {
|
||||||
|
"OData-MaxVersion": "4.0",
|
||||||
|
"OData-Version": "4.0",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Prefer":
|
||||||
|
'odata.include-annotations="*",return=representation',
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer " + token.access_token,
|
||||||
|
},
|
||||||
|
responseType: "stream",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const { response, attempts } = await retry(fetchStream, 3, 1000);
|
||||||
|
|
||||||
|
// Extract filename
|
||||||
|
const contentDisposition = response.headers["content-disposition"];
|
||||||
|
const filename = contentDisposition
|
||||||
|
? contentDisposition.split("filename=")[1]
|
||||||
|
: docRef.id;
|
||||||
|
|
||||||
|
res.setHeader(
|
||||||
|
"Content-Disposition",
|
||||||
|
`attachment; filename=${filename}`
|
||||||
|
);
|
||||||
|
res.setHeader("Content-Type", "application/octet-stream");
|
||||||
|
|
||||||
|
let totalBytes = 0;
|
||||||
|
|
||||||
|
response.data.on("data", (chunk) => {
|
||||||
|
totalBytes += chunk.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
response.data.pipe(res);
|
||||||
|
|
||||||
|
response.data.on("end", () => {
|
||||||
|
const durationMs = Date.now() - startTime;
|
||||||
|
console.log(
|
||||||
|
`[Download Complete] Document: ${filename}, ID: ${docRef.id}, Size: ${totalBytes} bytes, Duration: ${durationMs}ms, Attempts: ${attempts}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
response.data.on("error", (err) => {
|
||||||
|
consoleLogger(err);
|
||||||
|
if (!res.headersSent) res.redirect("/filenotavailable");
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
consoleLogger(error);
|
||||||
|
if (!res.headersSent) res.redirect("/filenotavailable");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
responseLimit: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApiProxy;
|
||||||
|
|||||||
@@ -1469,6 +1469,22 @@ textarea,
|
|||||||
top: calc(50% - 10px);
|
top: calc(50% - 10px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.documentLink {
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #0360a6;
|
||||||
|
display: block;
|
||||||
|
padding-left: 0px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $white;
|
||||||
|
color: #3b7dc5;
|
||||||
|
border: none;
|
||||||
|
padding-left: 0px
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.appealModule {
|
.appealModule {
|
||||||
@@ -2932,6 +2948,10 @@ fade {
|
|||||||
.govuk-label-s {
|
.govuk-label-s {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.govuk-checkboxes_short {
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user