diff --git a/actions/azurestorage.js b/actions/azurestorage.js index aa5617bc..a3c02dc0 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -1696,7 +1696,12 @@ export const listBlobHierarchical = async ( export async function listBlobHierarchicalForUser(containerClient) { const blobNames = []; for await (const blob of containerClient.listBlobsFlat()) { - blobNames.push(blob.name); + // blobNames.push(blob.name); + + blobNames.push({ + name: blob.name, + size: blob.properties.contentLength || 0, // in bytes + }); } return blobNames; } diff --git a/pages/api/file/downloadblob.js b/pages/api/file/downloadblob.js index 6067189a..5ad78d18 100644 --- a/pages/api/file/downloadblob.js +++ b/pages/api/file/downloadblob.js @@ -24,10 +24,19 @@ ApiProxy.get(async (req, res) => { "&blobname=" + blobName.trim(); + // console.log(checkquerypath); + + // console.log(hashAPIPath(checkquerypath)); + + // console.log(req.query); + if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { + const bloblocation = + casefolderID + (blobName.indexOf(".json") > 0 ? "/" : "/files/"); + const downloaded = await downloadFile( containerName, - casefolderID + "/files/" + decodeURI(blobName) + bloblocation + decodeURI(blobName) ); res.setHeader( diff --git a/pages/api/file/editRepJson.js b/pages/api/file/editRepJson.js new file mode 100644 index 00000000..65080769 --- /dev/null +++ b/pages/api/file/editRepJson.js @@ -0,0 +1,83 @@ +import { DefaultAzureCredential } from "@azure/identity"; +import { BlobServiceClient } from "@azure/storage-blob"; + +export default async function handler(req, res) { + if (req.method !== "POST") { + res.status(405).json({ error: "Method not allowed" }); + return; + } + + const { container, blobName } = req.body; + + if (!container || !blobName) { + res.status(400).json({ error: "Missing container or blobName" }); + return; + } + + try { + const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME; + const creds = new DefaultAzureCredential(); + const blobServiceClient = new BlobServiceClient( + `https://${accountName}.blob.core.windows.net`, + creds + ); + + const containerClient = blobServiceClient.getContainerClient(container); + const blockBlobClient = containerClient.getBlockBlobClient(blobName); + + // Download blob content + const downloadResponse = await blockBlobClient.download(0); + const downloaded = await streamToString( + downloadResponse.readableStreamBody + ); + + // Parse JSON + const json = JSON.parse(downloaded); + + // Remove repComplete element if it exists + if ("repComplete" in json) { + delete json.repComplete; + } + + const tags = { + containerid: json.containerID, + caseID: json.ticketnumber, + blobType: "Representation", + }; + + // Convert back to string + const updatedContent = JSON.stringify(json, null, 2); + + // Upload updated content (overwrite) + await blockBlobClient.upload( + updatedContent, + Buffer.byteLength(updatedContent), + { + blobHTTPHeaders: { blobContentType: "application/json" }, + } + ); + + console.log("the tags:", tags); + const withTags = await blockBlobClient.setTags(tags); + const withMeta = await blockBlobClient.setMetadata(tags); + + res.status(200).json({ message: "repComplete removed successfully" }); + } catch (error) { + console.error("Error editing rep.json:", error); + res.status(500).json({ error: "Failed to edit rep.json" }); + } +} + +// Helper function to read stream to string +async function streamToString(readableStream) { + return new Promise((resolve, reject) => { + const chunks = []; + readableStream.on("data", (data) => { + chunks.push(data.toString()); + }); + readableStream.on("end", () => { + resolve(chunks.join("")); + }); + readableStream.on("error", reject); + }); +} diff --git a/pages/storageAdmin.js b/pages/storageAdmin.js index 77cd088b..09eda695 100644 --- a/pages/storageAdmin.js +++ b/pages/storageAdmin.js @@ -7,10 +7,238 @@ import { listBlobHierarchical, listContainersForUser, } from "../actions/azurestorage"; - +import Link from "next/link"; import { PrismaClient } from "@prisma/client"; +import { + bytesToSize, + getThumbnailIconByExtension, + updateLinks, +} from "../components/utils"; +import { + consoleLogger, + getIP, + sendRepCompleteMessage, + deleteMyRepresentationsFromBlob, +} from "../actions"; +import CryptoJS from "crypto-js"; -import { consoleLogger, getIP } from "../actions"; +function getUpToLastSlash(url) { + let lastSlash = url.lastIndexOf("/"); + + url = url.substring(0, lastSlash); + + url = + url.lastIndexOf("/files") > 0 + ? url.substring(0, url.lastIndexOf("/files")) + : url; + return url; +} + +async function streamToString(readableStream) { + return new Promise((resolve, reject) => { + const chunks = []; + readableStream.on("data", (data) => { + chunks.push(data.toString()); + }); + readableStream.on("end", () => { + resolve(chunks.join("")); + }); + readableStream.on("error", reject); + }); +} + +const deleteRepItem = (containerID, caseID, repfile) => { + deleteMyRepresentationsFromBlob(containerID, caseID, repfile).then( + (data) => { + console.log("======== rep deleted ========="); + return data; + } + ); +}; + +export default function StoragePage({ data }) { + console.log(data); + return ( +
+
+

Storage Account Contents

+ {data.map((user) => ( +
+ {user.containers.map((c) => ( +
+
+

+ {c.container} - {user.email} +

+ {c.folders.map((folder) => ( +
+

+ {folder.folderPath || "Root"} +

+ {/* {folder.hasRepJson && ( +

+ rep.json file found:{" "} + {folder.repJsonBlob.name} +

+ )} */} + {folder.hasRepJson && ( +
+ {" "} + + {folder.repComplete && ( + + )} + {folder.repComplete && ( + + )} +
+ )} + +
    + {folder.blobs.map((blob) => ( +
  • + + {blob.name} ( + {bytesToSize( + blob.contentLength || + blob.size + )} + ) + +
  • + ))} +
+
+ ))} +
+
+ ))} +
+ ))} +
+
+ ); +} export async function getServerSideProps({ req }) { // Load allowed IPs from env and trim spaces @@ -54,41 +282,227 @@ export async function getServerSideProps({ req }) { const users = await prisma.user.findMany(); - const data = await Promise.all( + const hashAPIPath = (queryPath, WORDKEY) => { + var hashlink = CryptoJS.HmacSHA256( + queryPath, + CryptoJS.enc.Hex.parse(WORDKEY) + ); + hashlink = hashlink.toString(CryptoJS.enc.Hex); + return hashlink; + }; + + let data = await Promise.all( users.map(async (user) => { const containers = await listContainersForUser( blobServiceClient, `${user.id}`, user.email ); - return containers.length > 0 - ? { id: user.id, email: user.email, containers } + + // Add hashedName for each blob + // const containersWithHashes = containers.map((container) => { + // console.log("eee "); + + // const groupedByFolder = container.blobs.reduce((acc, blob) => { + // // Get folder path - everything before last slash, or "" if no slash + // const folderPath = getUpToLastSlash(blob.name.trim()) || ""; + + // const hashedfilepath = hashAPIPath( + // "/api/file/downloadblob?container=" + + // container.container + + // "&casefolderID=" + + // folderPath + + // "&blobname=" + + // blob.name + // .substring(blob.name.lastIndexOf("/") + 1) + // .trim(), + // process.env.HASHKEY + // ); + + // const blobWithHash = { ...blob, hashedfilepath }; + + // // Find or create folder group + // let folderGroup = acc.find( + // (f) => f.folderPath === folderPath + // ); + // if (!folderGroup) { + // folderGroup = { folderPath, blobs: [] }; + // acc.push(folderGroup); + // } + // folderGroup.blobs.push(blobWithHash); + + // return acc; + // }, []); + + // console.log(groupedByFolder); + // return { + // ...container, + // blobs: container.blobs.map((blob) => ({ + // ...blob, + // hashedfilepath: hashAPIPath( + // "/api/file/downloadblob?container=" + + // container.container + + // "&casefolderID=" + + // getUpToLastSlash(blob.name.trim()) + + // "&blobname=" + + // blob.name + // .substring(blob.name.lastIndexOf("/") + 1) + // .trim(), + // process.env.HASHKEY + // ), + // })), + // }; + // }); + + // const containersWithHashes = containers.map((container) => { + // const groupedByFolder = container.blobs.reduce((acc, blob) => { + // const folderPath = getUpToLastSlash(blob.name.trim()) || ""; + + // const hashedfilepath = hashAPIPath( + // "/api/file/downloadblob?container=" + + // container.container + + // "&casefolderID=" + + // folderPath + + // "&blobname=" + + // blob.name + // .substring(blob.name.lastIndexOf("/") + 1) + // .trim(), + // process.env.HASHKEY + // ); + + // const blobWithHash = { ...blob, hashedfilepath }; + + // let folderGroup = acc.find( + // (f) => f.folderPath === folderPath + // ); + // if (!folderGroup) { + // folderGroup = { + // folderPath, + // blobs: [], + // hasRepJson: false, // flag + // repJsonBlob: null, // save the rep.json blob if found + // }; + // acc.push(folderGroup); + // } + + // folderGroup.blobs.push(blobWithHash); + + // // Check if this blob ends with "rep.json" + // if (blob.name.toLowerCase().endsWith("rep.json")) { + // folderGroup.hasRepJson = true; + // folderGroup.repJsonBlob = blobWithHash; + // // You can also do some processing here if you want + // } + + // return acc; + // }, []); + + // return { + // ...container, + // folders: groupedByFolder, + // // Optionally remove flat blobs if you don't want them + // // blobs: undefined, + // }; + // }); + + const containersWithHashes = await Promise.all( + containers.map(async (container) => { + const groupedByFolder = await container.blobs.reduce( + async (accP, blob) => { + // Because we'll do async read, reduce needs to be async, so accP is a Promise + const acc = await accP; + + const folderPath = + getUpToLastSlash(blob.name.trim()) || ""; + + const hashedfilepath = hashAPIPath( + "/api/file/downloadblob?container=" + + container.container + + "&casefolderID=" + + folderPath + + "&blobname=" + + blob.name + .substring( + blob.name.lastIndexOf("/") + 1 + ) + .trim(), + process.env.HASHKEY + ); + + const blobWithHash = { ...blob, hashedfilepath }; + + let folderGroup = acc.find( + (f) => f.folderPath === folderPath + ); + if (!folderGroup) { + folderGroup = { + folderPath, + blobs: [], + hasRepJson: false, + repJsonBlob: null, + repComplete: false, // new flag + }; + acc.push(folderGroup); + } + + folderGroup.blobs.push(blobWithHash); + + // If this blob ends with rep.json, fetch and parse JSON + if (blob.name.toLowerCase().endsWith("rep.json")) { + folderGroup.hasRepJson = true; + folderGroup.repJsonBlob = blobWithHash; + + // Read blob content from Azure Storage + const containerClient = + blobServiceClient.getContainerClient( + container.container + ); + const blockBlobClient = + containerClient.getBlockBlobClient( + blob.name + ); + + try { + const downloadResponse = + await blockBlobClient.download(0); + const downloaded = await streamToString( + downloadResponse.readableStreamBody + ); + + const json = JSON.parse(downloaded); + folderGroup.repComplete = Boolean( + json.repComplete + ); + } catch (err) { + console.error( + "Error reading or parsing rep.json", + err + ); + folderGroup.repComplete = false; + } + } + + return acc; + }, + Promise.resolve([]) + ); + + return { + ...container, + folders: groupedByFolder, + }; + }) + ); + + return containersWithHashes.length > 0 + ? { + id: user.id, + email: user.email, + containers: containersWithHashes, + } : null; }) ); return { props: { data: data.filter(Boolean) } }; } -export default function StoragePage({ data }) { - return ( -
-

Storage Account Contents

- {data.map((user) => ( -
- {user.containers.map((c) => ( -
-

- {c.container} - {user.email} -

-
    - {c.blobs.map((blob) => ( -
  • {blob}
  • - ))} -
-
- ))} -
- ))} -
- ); -}