IO-3464 S3 Document Editor
Signed-off-by: Allan Carr <allan@imexsystems.ca>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
//import "tui-image-editor/dist/tui-image-editor.css";
|
//import "tui-image-editor/dist/tui-image-editor.css";
|
||||||
|
import axios from "axios";
|
||||||
import { Result } from "antd";
|
import { Result } from "antd";
|
||||||
import * as markerjs2 from "markerjs2";
|
import * as markerjs2 from "markerjs2";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
@@ -6,8 +7,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
import { handleUpload } from "../documents-upload-imgproxy/documents-upload-imgproxy.utility.js";
|
||||||
import { GenerateSrcUrl } from "../jobs-documents-gallery/job-documents.utility";
|
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
|
||||||
@@ -23,6 +23,9 @@ export function DocumentEditorComponent({ currentUser, bodyshop, document }) {
|
|||||||
const imgRef = useRef(null);
|
const imgRef = useRef(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [uploaded, setuploaded] = useState(false);
|
const [uploaded, setuploaded] = useState(false);
|
||||||
|
const [imageUrl, setImageUrl] = useState(null);
|
||||||
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
|
const [imageLoading, setImageLoading] = useState(true);
|
||||||
const markerArea = useRef(null);
|
const markerArea = useRef(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
@@ -55,7 +58,7 @@ export function DocumentEditorComponent({ currentUser, bodyshop, document }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (imgRef.current !== null) {
|
if (imgRef.current !== null && imageLoaded && !markerArea.current) {
|
||||||
// create a marker.js MarkerArea
|
// create a marker.js MarkerArea
|
||||||
markerArea.current = new markerjs2.MarkerArea(imgRef.current);
|
markerArea.current = new markerjs2.MarkerArea(imgRef.current);
|
||||||
|
|
||||||
@@ -78,7 +81,47 @@ export function DocumentEditorComponent({ currentUser, bodyshop, document }) {
|
|||||||
//markerArea.current.settings.displayMode = "inline";
|
//markerArea.current.settings.displayMode = "inline";
|
||||||
markerArea.current.show();
|
markerArea.current.show();
|
||||||
}
|
}
|
||||||
}, [triggerUpload]);
|
}, [triggerUpload, imageLoaded]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// When the document changes, fetch the image via axios so auth and base URL are applied
|
||||||
|
let isCancelled = false;
|
||||||
|
|
||||||
|
const loadImage = async () => {
|
||||||
|
if (!document || !document.id) return;
|
||||||
|
|
||||||
|
setImageLoaded(false);
|
||||||
|
setImageLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
"/media/imgproxy/original",
|
||||||
|
{ documentId: document.id },
|
||||||
|
{ responseType: "blob" }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isCancelled) return;
|
||||||
|
|
||||||
|
const blobUrl = URL.createObjectURL(response.data);
|
||||||
|
setImageUrl((prevUrl) => {
|
||||||
|
if (prevUrl) URL.revokeObjectURL(prevUrl);
|
||||||
|
return blobUrl;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch original image blob", error);
|
||||||
|
} finally {
|
||||||
|
if (!isCancelled) {
|
||||||
|
setImageLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadImage();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isCancelled = true;
|
||||||
|
};
|
||||||
|
}, [document]);
|
||||||
|
|
||||||
async function b64toBlob(url) {
|
async function b64toBlob(url) {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
@@ -87,16 +130,21 @@ export function DocumentEditorComponent({ currentUser, bodyshop, document }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{!loading && !uploaded && (
|
{!loading && !uploaded && imageUrl && (
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
src={GenerateSrcUrl(document)}
|
src={imageUrl}
|
||||||
alt="sample"
|
alt="sample"
|
||||||
crossOrigin="anonymous"
|
onLoad={() => setImageLoaded(true)}
|
||||||
|
onError={(error) => {
|
||||||
|
console.error("Failed to load original image", error);
|
||||||
|
}}
|
||||||
style={{ maxWidth: "90vw", maxHeight: "90vh" }}
|
style={{ maxWidth: "90vw", maxHeight: "90vh" }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{loading && <LoadingSpinner message={t("documents.labels.uploading")} />}
|
{(loading || imageLoading || !imageLoaded) && !uploaded && (
|
||||||
|
<LoadingSpinner message={t("documents.labels.uploading")} />
|
||||||
|
)}
|
||||||
{uploaded && <Result status="success" title={t("documents.successes.edituploaded")} />}
|
{uploaded && <Result status="success" title={t("documents.successes.edituploaded")} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const generateSignedUploadUrls = async (req, res) => {
|
|||||||
const client = new S3Client({ region: InstanceRegion() });
|
const client = new S3Client({ region: InstanceRegion() });
|
||||||
|
|
||||||
// Check if filename indicates PDF and set content type accordingly
|
// Check if filename indicates PDF and set content type accordingly
|
||||||
const isPdf = filename.toLowerCase().endsWith('.pdf');
|
const isPdf = filename.toLowerCase().endsWith(".pdf");
|
||||||
const commandParams = {
|
const commandParams = {
|
||||||
Bucket: imgproxyDestinationBucket,
|
Bucket: imgproxyDestinationBucket,
|
||||||
Key: key,
|
Key: key,
|
||||||
@@ -62,7 +62,7 @@ const generateSignedUploadUrls = async (req, res) => {
|
|||||||
// For PDFs, we need to add conditions to the presigned URL to enforce content type
|
// For PDFs, we need to add conditions to the presigned URL to enforce content type
|
||||||
const presignedUrlOptions = { expiresIn: 360 };
|
const presignedUrlOptions = { expiresIn: 360 };
|
||||||
if (isPdf) {
|
if (isPdf) {
|
||||||
presignedUrlOptions.signableHeaders = new Set(['content-type']);
|
presignedUrlOptions.signableHeaders = new Set(["content-type"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const presignedUrl = await getSignedUrl(client, command, presignedUrlOptions);
|
const presignedUrl = await getSignedUrl(client, command, presignedUrlOptions);
|
||||||
@@ -265,6 +265,82 @@ const downloadFiles = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream original image content by document ID
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
const getOriginalImageByDocumentId = async (req, res) => {
|
||||||
|
const {
|
||||||
|
body: { documentId },
|
||||||
|
user,
|
||||||
|
userGraphQLClient
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
if (!documentId) {
|
||||||
|
return res.status(400).json({ message: "documentId is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.log("imgproxy-original-image", "DEBUG", user?.email, null, { documentId });
|
||||||
|
|
||||||
|
const { documents } = await userGraphQLClient.request(GET_DOCUMENTS_BY_IDS, { documentIds: [documentId] });
|
||||||
|
|
||||||
|
if (!documents || documents.length === 0) {
|
||||||
|
return res.status(404).json({ message: "Document not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [document] = documents;
|
||||||
|
const { type } = document;
|
||||||
|
|
||||||
|
if (!type || !type.startsWith("image")) {
|
||||||
|
return res.status(400).json({ message: "Document is not an image" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const s3client = new S3Client({ region: InstanceRegion() });
|
||||||
|
const key = keyStandardize(document);
|
||||||
|
|
||||||
|
let s3Response;
|
||||||
|
try {
|
||||||
|
s3Response = await s3client.send(
|
||||||
|
new GetObjectCommand({
|
||||||
|
Bucket: imgproxyDestinationBucket,
|
||||||
|
Key: key
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.log("imgproxy-original-image-s3-error", "ERROR", user?.email, null, {
|
||||||
|
key,
|
||||||
|
message: err.message,
|
||||||
|
stack: err.stack
|
||||||
|
});
|
||||||
|
return res.status(400).json({ message: "Unable to retrieve image" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader("Content-Type", type || "image/jpeg");
|
||||||
|
|
||||||
|
s3Response.Body.on("error", (err) => {
|
||||||
|
logger.log("imgproxy-original-image-s3stream-error", "ERROR", user?.email, null, {
|
||||||
|
key,
|
||||||
|
message: err.message,
|
||||||
|
stack: err.stack
|
||||||
|
});
|
||||||
|
res.destroy(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
s3Response.Body.pipe(res);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("imgproxy-original-image-error", "ERROR", req.user?.email, null, {
|
||||||
|
documentId,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(400).json({ message: error.message, stack: error.stack });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete Files
|
* Delete Files
|
||||||
* @param req
|
* @param req
|
||||||
@@ -425,6 +501,7 @@ const keyStandardize = (doc) => {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
generateSignedUploadUrls,
|
generateSignedUploadUrls,
|
||||||
getThumbnailUrls,
|
getThumbnailUrls,
|
||||||
|
getOriginalImageByDocumentId,
|
||||||
downloadFiles,
|
downloadFiles,
|
||||||
deleteFiles,
|
deleteFiles,
|
||||||
moveFiles
|
moveFiles
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const { createSignedUploadURL, downloadFiles, renameKeys, deleteFiles } = requir
|
|||||||
const {
|
const {
|
||||||
generateSignedUploadUrls: createSignedUploadURLImgproxy,
|
generateSignedUploadUrls: createSignedUploadURLImgproxy,
|
||||||
getThumbnailUrls: getThumbnailUrlsImgproxy,
|
getThumbnailUrls: getThumbnailUrlsImgproxy,
|
||||||
|
getOriginalImageByDocumentId: getOriginalImageByDocumentIdImgproxy,
|
||||||
downloadFiles: downloadFilesImgproxy,
|
downloadFiles: downloadFilesImgproxy,
|
||||||
moveFiles: moveFilesImgproxy,
|
moveFiles: moveFilesImgproxy,
|
||||||
deleteFiles: deleteFilesImgproxy
|
deleteFiles: deleteFilesImgproxy
|
||||||
@@ -24,5 +25,6 @@ router.post("/imgproxy/thumbnails", getThumbnailUrlsImgproxy);
|
|||||||
router.post("/imgproxy/download", downloadFilesImgproxy);
|
router.post("/imgproxy/download", downloadFilesImgproxy);
|
||||||
router.post("/imgproxy/rename", moveFilesImgproxy);
|
router.post("/imgproxy/rename", moveFilesImgproxy);
|
||||||
router.post("/imgproxy/delete", deleteFilesImgproxy);
|
router.post("/imgproxy/delete", deleteFilesImgproxy);
|
||||||
|
router.post("/imgproxy/original", getOriginalImageByDocumentIdImgproxy);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
Reference in New Issue
Block a user