+// // {downloadStatus}
+// // {/* {progress !== null && ` (${progress}%)`} */}
+// //
+// // )}
+// // >
+// // );
+// // }
+// 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 ? (
+//
+// {status === "downloading" && progress !== null
+// ? `Downloading (${progress}%)`
+// : status === "queued"
+// ? "Queued"
+// : status === "done"
+// ? "Download complete!"
+// : status === "failed"
+// ? "Download failed"
+// : ""}
+//
+// )}
+// >
+// );
+// }
+
+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 (
+
+ {detailsObj.pinswg_publishtoweb ? (
+
+ ) : (
+
+ {detailsObj.pinswg_name || ""}
+
+ )}
+
+ {status !== "idle" && (
+
+ {status === "downloading" && progress !== null
+ ? `Downloading `
+ : status === "queued"
+ ? "Queued"
+ : status === "done"
+ ? "Download complete!"
+ : status === "failed"
+ ? "Download failed"
+ : ""}
+
+ )}
+
+ );
+}
diff --git a/pages/api/documents/download/[id].js b/pages/api/documents/download/[id].js
index 044e62b9..cbc8db9c 100644
--- a/pages/api/documents/download/[id].js
+++ b/pages/api/documents/download/[id].js
@@ -1,110 +1,203 @@
-/**
- * @swagger
- * /api/documents/download/{id}:
- * get:
- * tags:
- * - Documents
- * description: Get document
- * parameters:
- * - name: id
- * in: path
- * description: iShare ID
- * type: string
- * required: true
- * default: A41567436
- * - name: hash
- * in: query
- * description: Hash string
- * default: e0a1c9cd2817826a25bca67e60b807eae747cbed769e3ec42cf997541c32be71
- * responses:
- * 200:
- * description: Success
- */
+// /**
+// * @swagger
+// * /api/documents/download/{id}:
+// * get:
+// * tags:
+// * - Documents
+// * description: Get document
+// * parameters:
+// * - name: id
+// * in: path
+// * description: iShare ID
+// * type: string
+// * required: true
+// * default: A41567436
+// * - name: hash
+// * in: query
+// * description: Hash string
+// * default: e0a1c9cd2817826a25bca67e60b807eae747cbed769e3ec42cf997541c32be71
+// * responses:
+// * 200:
+// * 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 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");
- });
+// Retry utility with logging
+async function retry(fn, retries = 3, delay = 1000) {
+ for (let attempt = 1; attempt <= retries; attempt++) {
+ try {
+ return { response: await fn(), attempts: attempt };
+ } catch (err) {
+ if (attempt === retries) throw err;
+ console.log(`Retry ${attempt} failed, retrying in ${delay}ms...`);
+ await new Promise((res) => setTimeout(res, delay));
+ delay *= 2; // exponential backoff
+ }
+ }
}
+
+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;
diff --git a/styles/sass/welshgov/_application.scss b/styles/sass/welshgov/_application.scss
index b80401b7..9e2f3b14 100644
--- a/styles/sass/welshgov/_application.scss
+++ b/styles/sass/welshgov/_application.scss
@@ -1469,6 +1469,22 @@ textarea,
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 {
@@ -2932,6 +2948,10 @@ fade {
.govuk-label-s {
font-size: 14px;
}
+
+ &.govuk-checkboxes_short {
+ height: 180px;
+ }
}