// components/admin/utils/serverStorage.js import { DefaultAzureCredential } from "@azure/identity"; import { BlobServiceClient } from "@azure/storage-blob"; import { PrismaClient } from "@prisma/client"; import CryptoJS from "crypto-js"; import { getUpToLastSlash, streamToString } from "../utils/adminHelper"; import { listContainersForUser } from "../../../actions/azurestorage"; import pLimit from "p-limit"; const prisma = global.prisma || new PrismaClient(); const WORDKEY = process.env.HASHKEY; /** * Generate HMAC hash for blob API link. */ function hashAPIPath(queryPath, WORDKEY) { let hashlink = CryptoJS.HmacSHA256( queryPath, CryptoJS.enc.Hex.parse(WORDKEY) ); return hashlink.toString(CryptoJS.enc.Hex); } /** * Fetch blob structure + users for admin storage page. */ export async function fetchAdminStorageData() { const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME; const creds = new DefaultAzureCredential(); const blobServiceClient = new BlobServiceClient( `https://${accountName}.blob.core.windows.net`, creds ); const prisma = global.prisma || new PrismaClient(); const users = await prisma.user.findMany(); const limit = pLimit(5); let data = await Promise.all( users.map(async (user) => { limit(async () => { const containers = await listContainersForUser( blobServiceClient, `${user.id}`, user.email ); const containersWithHashes = await Promise.all( containers.map(async (container) => { const groupedByFolder = await container.blobs.reduce( async (accP, blob) => { 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 fileNameOnly = blob.name.substring( blob.name.lastIndexOf("/") + 1 ); const isTmpFile = fileNameOnly.startsWith("TMP-"); const blobWithHash = { ...blob, hashedfilepath, isTmpFile }; let folderGroup = acc.find( (f) => f.folderPath === folderPath ); if (!folderGroup) { folderGroup = { folderPath, blobs: [], hasRepJson: false, repJsonBlob: null, repComplete: false, hasTmpFile: false }; acc.push(folderGroup); } folderGroup.blobs.push(blobWithHash); if (isTmpFile) { folderGroup.hasTmpFile = true; } // rep.json handling if ( blob.name.toLowerCase().endsWith("rep.json") ) { folderGroup.hasRepJson = true; folderGroup.repJsonBlob = blobWithHash; 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 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; }); }) ); data = data.filter(Boolean); let userData = users .filter((user) => user.email && user.email.trim() !== "") .map((user) => ({ id: user.id, email: user.email, created: user.emailVerified?.toLocaleString("en-GB") || "" })); let repCompleteCount = 0; data.forEach((user) => { user.containers.forEach((container) => { container.folders.forEach((folder) => { if (folder.repComplete === true) repCompleteCount++; }); }); }); return { data, userData, repCompleteCount }; }