From 8c34e6422a12750bf2f142b21efe2c295fdc0057 Mon Sep 17 00:00:00 2001 From: Arshad Mohammed <87503056+arshadparwaiz@users.noreply.github.com> Date: Mon, 13 May 2024 21:24:21 -0700 Subject: [PATCH] MWPW-145191 - Promote Graybox files after MDAST cleanup (#11) - Uploading files after MDAST processing to the Default folder from Graybox folder - Promote Copy files without graybox styles from Graybox to Default Folder --- actions/config.js | 2 +- actions/docxUpdater.js | 106 +- actions/graybox/promote-worker.js | 82 +- actions/helixUtils.js | 17 +- actions/sharepoint.js | 150 ++- actions/utils.js | 4 +- defaultstyles.xml.js | 1834 +++++++++++++++++++++++++++++ package-lock.json | 2 +- package.json | 2 +- 9 files changed, 2110 insertions(+), 89 deletions(-) create mode 100644 defaultstyles.xml.js diff --git a/actions/config.js b/actions/config.js index 9f548d4..1ac216b 100644 --- a/actions/config.js +++ b/actions/config.js @@ -14,7 +14,6 @@ * is strictly forbidden unless prior written permission is obtained * from Adobe. ************************************************************************* */ - const appConfig = require('./appConfig'); const GRAPH_API = 'https://graph.microsoft.com/v1.0'; @@ -103,6 +102,7 @@ function getHelixAdminConfig() { async function getConfig() { if (appConfig.getUrlInfo().isValid()) { const applicationConfig = appConfig.getConfig(); + return { sp: getSharepointConfig(applicationConfig), admin: getHelixAdminConfig(), diff --git a/actions/docxUpdater.js b/actions/docxUpdater.js index 03a48e3..744b189 100644 --- a/actions/docxUpdater.js +++ b/actions/docxUpdater.js @@ -17,16 +17,16 @@ const parseMarkdown = require('milo-parse-markdown').default; const { mdast2docx } = require('../node_modules/milo-md2docx/lib/index'); const { getAioLogger } = require('./utils'); -const { fetchWithRetry } = require('./sharepoint'); -const gbStyleExpression = 'gb-';//graybox style expression. need to revisit if there are any more styles to be considered. +const DEFAULT_STYLES = require('../defaultstyles.xml'); + +const gbStyleExpression = 'gb-'; // graybox style expression. need to revisit if there are any more styles to be considered. const emptyString = ''; const grayboxStylesRegex = new RegExp('gb-[a-zA-Z0-9,._-]*', 'g'); const gbDomainSuffix = '-graybox'; const logger = getAioLogger(); let firstGtRows = []; - /** * Updates a document based on the provided Markdown file path, experience name, and options. * @param {string} mdPath - The path to the Markdown file. @@ -34,23 +34,27 @@ let firstGtRows = []; * @param {object} options - The options for fetching the Markdown file. * @returns {Promise} - A promise that resolves to the generated Docx file. */ -async function updateDocument(mdPath, expName, options = {}){ +async function updateDocument(content, expName, hlxAdminApiKey) { firstGtRows = []; - const response = await fetchWithRetry(`${mdPath}`, options); - const content = await response.text(); - if (content.includes(expName) || content.includes(gbStyleExpression) || content.includes(gbDomainSuffix)) { - const state = { content: { data: content }, log: '' }; - await parseMarkdown(state); - const { mdast } = state.content; - updateExperienceNameFromLinks(mdast.children, expName); - logger.info('Experience name removed from links'); - iterateGtRowsToReplaceStyles(); - logger.info('Graybox styles removed'); - //generated docx file from updated mdast - const docx = await generateDocxFromMdast(mdast); - //TODO promote this docx file - logger.info('Mdast to Docx file conversion done'); + let docx; + + const state = { content: { data: content }, log: '' }; + await parseMarkdown(state); + const { mdast } = state.content; + updateExperienceNameFromLinks(mdast.children, expName); + + iterateGtRowsToReplaceStyles(); + + try { + // generated docx file from updated mdast + docx = await generateDocxFromMdast(mdast, hlxAdminApiKey); + } catch (err) { + // Mostly bad string ignored + logger.debug(`Error while generating docxfromdast ${err}`); } + + logger.info('Mdast to Docx file conversion done'); + return docx; } /** @@ -62,47 +66,47 @@ async function updateDocument(mdPath, expName, options = {}){ const updateExperienceNameFromLinks = (mdast, expName) => { if (mdast) { mdast.forEach((child) => { - if (child.type === 'gridTable') { - firstGtRows.push(findFirstGtRowInNode(child)); - } - //remove experience name from links on the document - if (child.type === 'link' && child.url && (child.url.includes(expName) || child.url.includes(gbDomainSuffix))) { - child.url = child.url.replaceAll(`/${expName}/`, '/').replaceAll(gbDomainSuffix, emptyString); - logger.info(`Link updated: ${child.url}`); - } - if (child.children) { - updateExperienceNameFromLinks(child.children, expName); - } + if (child.type === 'gridTable') { + firstGtRows.push(findFirstGtRowInNode(child)); + } + // remove experience name from links on the document + if (child.type === 'link' && child.url && (child.url.includes(expName) || child.url.includes(gbDomainSuffix))) { + child.url = child.url.replaceAll(`/${expName}/`, '/').replaceAll(gbDomainSuffix, emptyString); + } + if (child.children) { + updateExperienceNameFromLinks(child.children, expName); } - ); + }); } -} +}; /** * Helper function, iterates through the firstGtRows array and replaces graybox styles for each row. */ const iterateGtRowsToReplaceStyles = () => { - firstGtRows.forEach((gtRow) => { - if (gtRow && gtRow.children) { - replaceGrayboxStyles(gtRow); - } - }); -} + try { + firstGtRows.forEach((gtRow) => { + if (gtRow && gtRow.children) { + replaceGrayboxStyles(gtRow); + } + }); + } catch (err) { + // Mostly bad string ignored + logger().debug(`Error while iterating GTRows to replaces styles ${err}`); + } +}; /** * Replaces all graybox styles from blocks and text. - * + * * @param {object} node - The node to process. * @returns {void} */ const replaceGrayboxStyles = (node) => { - //replace all graybox styles from blocks and text + // replace all graybox styles from blocks and text if (node && node.type === 'text' && node.value && node.value.includes(gbStyleExpression)) { - logger.info(node); node.value = node.value.replace(grayboxStylesRegex, emptyString) .replace('()', emptyString).replace(', )', ')'); - logger.info('updated value>> '); - logger.info(node); return; } if (node.children) { @@ -110,7 +114,7 @@ const replaceGrayboxStyles = (node) => { replaceGrayboxStyles(child); }); } -} +}; /** * Finds the first 'gtRow' node in the given node or its children. @@ -126,17 +130,25 @@ function findFirstGtRowInNode(node) { return findFirstGtRowInNode(child); } } + return null; } - /** * Generate a Docx file from the given mdast. * @param {Object} mdast - The mdast representing the document. * @returns {Promise} A promise that resolves to the generated Docx file. */ -async function generateDocxFromMdast(mdast) { - logger.info('Docx file Docx file generation from mdast started...'); - return await mdast2docx(mdast); +async function generateDocxFromMdast(mdast, hlxAdminApiKey) { + const options = { + stylesXML: DEFAULT_STYLES, + auth: { + authorization: `token ${hlxAdminApiKey}`, + } + }; + + const docx = await mdast2docx(mdast, options); + + return docx; } module.exports = updateDocument; diff --git a/actions/graybox/promote-worker.js b/actions/graybox/promote-worker.js index d1455b4..d20696c 100644 --- a/actions/graybox/promote-worker.js +++ b/actions/graybox/promote-worker.js @@ -15,21 +15,24 @@ * from Adobe. ************************************************************************* */ -const { getAioLogger, handleExtension, logMemUsage, delay, isFilePatternMatched, toUTCStr } = require('../utils'); +const fetch = require('node-fetch'); +const { + getAioLogger, handleExtension, isFilePatternMatched, toUTCStr +} = require('../utils'); const appConfig = require('../appConfig'); const { getConfig } = require('../config'); -const { getAuthorizedRequestOption, fetchWithRetry, updateExcelTable, bulkCreateFolders } = require('../sharepoint'); +const { getAuthorizedRequestOption, fetchWithRetry, updateExcelTable } = require('../sharepoint'); const helixUtils = require('../helixUtils'); -const sharepointAuth = require('../sharepointAuth'); const updateDocument = require('../docxUpdater'); -const fetch = require('node-fetch'); - +const { saveFile, copyFile, promoteCopy } = require('../sharepoint'); const logger = getAioLogger(); const MAX_CHILDREN = 1000; const IS_GRAYBOX = true; const BATCH_REQUEST_PREVIEW = 200; -const DELAY_TIME_COPY = 3000; + +const gbStyleExpression = 'gb-'; // graybox style expression. need to revisit if there are any more styles to be considered. +const gbDomainSuffix = '-graybox'; async function main(params) { logger.info('Graybox Promote Worker invoked'); @@ -54,41 +57,71 @@ async function main(params) { logger.info(`Files in graybox folder in ${experienceName}`); logger.info(JSON.stringify(gbFiles)); - // create batches to process the data const batchArray = []; for (let i = 0; i < gbFiles.length; i += BATCH_REQUEST_PREVIEW) { const arrayChunk = gbFiles.slice(i, i + BATCH_REQUEST_PREVIEW); batchArray.push(arrayChunk); } - + // process data in batches const previewStatuses = []; - let failedPreviews = ''; + let failedPreviews = []; + const promoteStatuses = []; + const failedPromoteStatuses = []; if (helixUtils.canBulkPreview()) { const paths = []; batchArray.forEach((batch) => { batch.forEach((gbFile) => paths.push(handleExtension(gbFile.filePath))); }); previewStatuses.push(await helixUtils.bulkPreview(paths, helixUtils.getOperations().PREVIEW, experienceName)); - logger.info(`Preview Statuses >> ${JSON.stringify(previewStatuses)}`); - const failedPreviews = previewStatuses.filter((status) => !status.success).map((status) => status.path); - const urlInfo = appConfig.getUrlInfo(); + failedPreviews = previewStatuses.filter((status) => !status.success).map((status) => status.path); + + const helixAdminApiKey = helixUtils.getAdminApiKey(); + const options = {}; if (helixUtils.getAdminApiKey()) { options.headers = new fetch.Headers(); options.headers.append('Authorization', `token ${helixUtils.getAdminApiKey()}`); } - + // iterate through preview statuses and log success previewStatuses.forEach((status) => { - //check if status is an array and iterate through the array + // check if status is an array and iterate through the array if (Array.isArray(status)) { - status.forEach((stat) => { - logger.info(`status >> ${JSON.stringify(stat)}`); + status.forEach(async (stat) => { if (stat.success && stat.mdPath) { - logger.info(`Preview success and mdPath for file: ${stat.path} & ${stat.mdPath}`); - updateDocument(stat.mdPath, experienceName, options); + const response = await fetchWithRetry(`${stat.mdPath}`, options); + const content = await response.text(); + let docx; + const { sp } = await getConfig(); + + if (content.includes(experienceName) || content.includes(gbStyleExpression) || content.includes(gbDomainSuffix)) { + docx = await updateDocument(content, experienceName, helixAdminApiKey); + if (docx) { + // Save file Destination full path with file name and extension + const destinationFilePath = `${stat.path.substring(0, stat.path.lastIndexOf('/') + 1).replace('/'.concat(experienceName), '')}${stat.fileName}`; + + const saveStatus = await saveFile(docx, destinationFilePath, sp); + if (saveStatus && saveStatus.success === true) { + promoteStatuses.push(destinationFilePath); + } else { + failedPromoteStatuses.push(destinationFilePath); + } + } else { + logger.error(`Error generating docx file for ${stat.path}`); + } + } else { + const copySourceFilePath = `${stat.path.substring(0, stat.path.lastIndexOf('/') + 1)}${stat.fileName}`; // Copy Source full path with file name and extension + const copyDestinationFolder = `${stat.path.substring(0, stat.path.lastIndexOf('/')).replace('/'.concat(experienceName), '')}`; // Copy Destination folder path, no file name + const promoteCopyFileStatus = await promoteCopy(copySourceFilePath, copyDestinationFolder, stat.fileName, sp); + + if (promoteCopyFileStatus) { + promoteStatuses.push(`${copyDestinationFolder}/${stat.fileName}`); + } else { + failedPromoteStatuses.push(`${copyDestinationFolder}/${stat.fileName}`); + } + } } }); } @@ -99,10 +132,19 @@ async function main(params) { logger.info('Updating project excel file with status'); const curreDateTime = new Date(); const { projectExcelPath } = appConfig.getPayload(); - const sFailedPreviews = failedPreviews.length > 0 ? 'Failed Previews: \n' + failedPreviews.join('\n') : ''; + const sFailedPreviews = failedPreviews.length > 0 ? `Failed Previews: \n${failedPreviews.join('\n')}` : ''; const excelValues = [['Preview', toUTCStr(curreDateTime), sFailedPreviews]]; + // Update Preview Status await updateExcelTable(projectExcelPath, 'PROMOTE_STATUS', excelValues, IS_GRAYBOX); - logger.info('Project excel file updated with promote status.'); + + // Update Promote Status + const sPromoteStatuses = promoteStatuses.length > 0 ? `Promotes: \n${promoteStatuses.join('\n')}` : ''; + const sFailedPromoteStatuses = failedPromoteStatuses.length > 0 ? `Failed Promotes: \n${failedPromoteStatuses.join('\n')}` : ''; + const promoteExcelValues = [['Promote', toUTCStr(curreDateTime), sPromoteStatuses]]; + const failedPromoteExcelValues = [['Promote', toUTCStr(curreDateTime), sFailedPromoteStatuses]]; + + await updateExcelTable(projectExcelPath, 'PROMOTE_STATUS', promoteExcelValues, IS_GRAYBOX); + await updateExcelTable(projectExcelPath, 'PROMOTE_STATUS', failedPromoteExcelValues, IS_GRAYBOX); const responsePayload = 'Graybox Promote Worker action completed.'; return exitAction({ diff --git a/actions/helixUtils.js b/actions/helixUtils.js index 5f55481..369ca8d 100644 --- a/actions/helixUtils.js +++ b/actions/helixUtils.js @@ -66,7 +66,11 @@ class HelixUtils { * @returns List of path with preview/pubish status e.g. [{path:'/draft/file1', success: true}..] */ async bulkPreview(paths, operation, grayboxExperienceName, retryAttempt = 1) { - let prevStatuses = paths.filter((p) => p).map((path) => ({ success: false, path, resourcePath: '', responseCode: '' })); + let prevStatuses = paths.filter((p) => p).map((path) => ( + { + success: false, path, fileName: '', resourcePath: '', responseCode: '' + } + )); if (!prevStatuses.length) { return prevStatuses; } @@ -98,14 +102,14 @@ class HelixUtils { const jobResp = await response.json(); const jobName = jobResp.job?.name; if (jobName) { - logger.info(`check again jobName : ${jobName} operation : ${operation} repo : ${repo}`); const jobStatus = await this.bulkJobStatus(jobName, operation, repo); - logger.info(`jobStatus : ${JSON.stringify(jobStatus)}`); prevStatuses.forEach((e) => { logger.info(`Job details : ${jobName} / ${jobResp.messageId} / ${jobResp.job?.state}`); if (jobStatus[e.path]?.success) { e.success = true; + e.fileName = jobStatus[e.path]?.fileName; e.resourcePath = jobStatus[e.path]?.resourcePath; + e.mdPath = `https://${urlInfo.getBranch()}--${urlInfo.getRepo()}--${urlInfo.getOwner()}.hlx.page${e.resourcePath}`; } e.responseCode = jobStatus[e.path]?.responseCode; @@ -131,7 +135,6 @@ class HelixUtils { * @returns List of path with preview/pubish status e.g. ['/draft/file1': {success: true}..] */ async bulkJobStatus(jobName, operation, repo, bulkPreviewStatus = {}, retryAttempt = 1) { - logger.info(`Checking job status of ${jobName} for ${operation}`); try { const { helixAdminApiKeys } = appConfig.getConfig(); const options = {}; @@ -148,10 +151,10 @@ class HelixUtils { await this.bulkJobStatus(jobName, operation, repo, bulkPreviewStatus, retryAttempt + 1); } else if (response.ok) { const jobStatusJson = await response.json(); - logger.info(`jobStatusJson ${JSON.stringify(jobStatusJson)}`); - logger.info(`${operation} progress ${JSON.stringify(jobStatusJson.progress)}`); jobStatusJson.data?.resources?.forEach((rs) => { - bulkPreviewStatus[rs.path] = { success: JOB_STATUS_CODES.includes(rs.status), resourcePath: rs?.resourcePath, responseCode: rs.status }; + bulkPreviewStatus[rs.path] = { + success: JOB_STATUS_CODES.includes(rs.status), fileName: rs?.source?.name, resourcePath: rs?.resourcePath, responseCode: rs.status + }; }); if (jobStatusJson.state !== 'stopped' && !jobStatusJson.cancelled && retryAttempt <= appConfig.getConfig().maxBulkPreviewChecks) { diff --git a/actions/sharepoint.js b/actions/sharepoint.js index ad0f997..0963a48 100644 --- a/actions/sharepoint.js +++ b/actions/sharepoint.js @@ -144,12 +144,15 @@ async function getFileUsingDownloadUrl(downloadUrl) { } async function createFolder(folder, isGraybox) { + const logger = getAioLogger(); + logger.info(`Creating folder ${folder}`); const { sp } = await getConfig(); const options = await getAuthorizedRequestOption({ method: sp.api.directory.create.method }); options.body = JSON.stringify(sp.api.directory.create.payload); const baseURI = isGraybox ? sp.api.directory.create.gbBaseURI : sp.api.directory.create.baseURI; const res = await fetchWithRetry(`${baseURI}${folder}`, options); + logger.info(`Created folder ${folder} with status ${res.status}`); if (res.ok) { return res.json(); } @@ -167,23 +170,37 @@ function getFileNameFromPath(path) { return path.split('/').pop().split('/').pop(); } +/** + * Create Upload Session + * @param {*} sp sharepoint config + * @param {*} file file object to be uploaded + * @param {*} dest destination/target file full path with folder and filename with extension + * @param {*} filename filename with extension + * @param {*} isGraybox is graybox flag + * @returns upload session json object + */ async function createUploadSession(sp, file, dest, filename, isGraybox) { + const logger = getAioLogger(); + let fileSize = file.size; if (Buffer.isBuffer(file)) { fileSize = Buffer.byteLength(file); } + const payload = { ...sp.api.file.createUploadSession.payload, description: 'Preview file', fileSize, name: filename, }; + const options = await getAuthorizedRequestOption({ method: sp.api.file.createUploadSession.method }); options.body = JSON.stringify(payload); const baseURI = isGraybox ? sp.api.file.createUploadSession.gbBaseURI : sp.api.file.createUploadSession.baseURI; const createdUploadSession = await fetchWithRetry(`${baseURI}${dest}:/createUploadSession`, options); + return createdUploadSession.ok ? createdUploadSession.json() : undefined; } @@ -223,6 +240,11 @@ async function releaseUploadSession(sp, uploadUrl) { await deleteFile(sp, uploadUrl); } +/** + * Get Locked File New Name + * @param {*} filename original file name + * @returns new locked file name with timestamp + */ function getLockedFileNewName(filename) { const extIndex = filename.indexOf('.'); const fileNameWithoutExtn = filename.substring(0, extIndex); @@ -230,7 +252,18 @@ function getLockedFileNewName(filename) { return `${fileNameWithoutExtn}-locked-${Date.now()}${fileExtn}`; } +/** + * Create session and upload file + * @param {*} sp sharepoint config + * @param {*} file file object to be uploaded + * @param {*} dest destination/target file full path with folder and filename with extension + * @param {*} filename filename with extension + * @param {*} isGraybox is graybox flag + * @returns upload status object + */ async function createSessionAndUploadFile(sp, file, dest, filename, isGraybox) { + const logger = getAioLogger(); + logger.info(`Creating session and uploading file ${filename} to ${dest}`); const createdUploadSession = await createUploadSession(sp, file, dest, filename, isGraybox); const status = {}; if (createdUploadSession) { @@ -273,14 +306,12 @@ async function bulkCreateFolders(srcPathList, isGraybox) { }).filter((e) => true && e); const uniqPathLst = Array.from(new Set(allPaths)); const leafPathLst = uniqPathLst.filter((e) => uniqPathLst.findIndex((e1) => e1.indexOf(`${e}/`) >= 0) < 0); - // logger.info(`Unique path list ${JSON.stringify(leafPathLst)}`); try { logger.info('bulkCreateFolders started'); const promises = leafPathLst.map((folder) => createFolder(folder, isGraybox)); logger.info('Got createfolder promises and waiting....'); createtFolderStatuses.push(...await Promise.all(promises)); logger.info(`bulkCreateFolders completed ${createtFolderStatuses?.length}`); - // logger.info(`bulkCreateFolders statuses ${JSON.stringify(createtFolderStatuses)}`); } catch (error) { logger.info('Error while creating folders'); logger.info(error?.stack); @@ -289,12 +320,29 @@ async function bulkCreateFolders(srcPathList, isGraybox) { return createtFolderStatuses; } -async function copyFile(srcPath, destinationFolder, newName, isGraybox, isGrayboxLockedFile) { +/** + * Copy File + * @param {*} srcPath source file full path with folder and filename with extension + * @param {*} destinationFolder destination folder path + * @param {*} newName new file name + * @param {*} isGraybox is graybox flag + * @param {*} isGrayboxLockedFile is graybox locked file flag + * @param {*} spConfig sharepoint config + * @returns copy status true/false for the file + */ +async function copyFile(srcPath, destinationFolder, newName, isGraybox, isGrayboxLockedFile, spConfig) { const logger = getAioLogger(); - const { sp } = await getConfig(); - const { baseURI, gbBaseURI } = sp.api.file.copy; - const rootFolder = isGraybox ? gbBaseURI.split('/').pop() : baseURI.split('/').pop(); + logger.info(`In copy function: ${srcPath} to ${destinationFolder} with ${newName}`); + let sp; + if (spConfig) { + sp = spConfig; + } else { + sp = await getConfig().sp; + } + const { baseURI, gbBaseURI } = sp.api.file.copy; + const rootFolder = isGraybox ? gbBaseURI.split('/').pop() : baseURI.split('/').pop(); logger.info(`Copying file ${srcPath} to ${destinationFolder}`); + logger.info(`Copying file ${srcPath} to ${destinationFolder}`); const payload = { ...sp.api.file.copy.payload, parentReference: { path: `${rootFolder}${destinationFolder}` } }; if (newName) { payload.name = newName; @@ -306,6 +354,8 @@ async function copyFile(srcPath, destinationFolder, newName, isGraybox, isGraybo // In case of copy action triggered via saveFile(), locked file copy happens in the graybox content location // So baseURI is updated to reflect the destination accordingly const contentURI = isGraybox && isGrayboxLockedFile ? gbBaseURI : baseURI; + // const contentURI = isGraybox ? gbBaseURI : baseURI; + const copyStatusInfo = await fetchWithRetry(`${contentURI}${srcPath}:/copy?@microsoft.graph.conflictBehavior=replace`, options); const statusUrl = copyStatusInfo.headers.get('Location'); let copySuccess = false; @@ -325,13 +375,33 @@ async function copyFile(srcPath, destinationFolder, newName, isGraybox, isGraybo return copySuccess; } -async function saveFile(file, dest, isGraybox) { +/** + * Save File + * Also handles the locked files by renaming the locked file, copying it to a new file called -locked-, + * then reuploads the original file and then deleting the renamed locked file. + * @param {*} file file object to be saved + * @param {*} dest destination file full path with folder and filename with extension + * @param {*} spConfig sharepoint config + * @param {*} isGraybox is graybox flag + * @returns save file status true/false for the file & the path of the file + */ +async function saveFile(file, dest, spConfig, isGraybox) { + const logger = getAioLogger(); + try { const folder = getFolderFromPath(dest); const filename = getFileNameFromPath(dest); - await createFolder(folder, isGraybox); - const { sp } = await getConfig(); + logger.info(`Saving file ${filename} to ${folder}`); + // await createFolder(folder, isGraybox); + let sp; + if (spConfig) { + sp = spConfig; + } else { + sp = await getConfig().sp; + } + let uploadFileStatus = await createSessionAndUploadFile(sp, file, dest, filename, isGraybox); + if (uploadFileStatus.locked) { await releaseUploadSession(sp, uploadFileStatus.sessionUrl); const lockedFileNewName = getLockedFileNewName(filename); @@ -339,7 +409,8 @@ async function saveFile(file, dest, isGraybox) { const spFileUrl = `${baseURI}${dest}`; await renameFile(spFileUrl, lockedFileNewName); const newLockedFilePath = `${folder}/${lockedFileNewName}`; - const copyFileStatus = await copyFile(newLockedFilePath, folder, filename, isGraybox, true); + const copyFileStatus = await copyFile(newLockedFilePath, folder, filename, isGraybox, true, sp); + if (copyFileStatus) { uploadFileStatus = await createSessionAndUploadFile(sp, file, dest, filename, isGraybox); if (uploadFileStatus.success) { @@ -357,6 +428,64 @@ async function saveFile(file, dest, isGraybox) { return { success: false, path: dest }; } +/** + * Promote Copy + * Copies the Graaybox files back to the main content tree. + * Creates intermediate folders if needed. +* @param {*} srcPath Graybox Source Path + * @param {*} destinationFolder Promote Destination Folder + * @param {*} fileName FileName to be promoted + * @param {*} sp sharepoint config + * @returns promote status true/false for the file + */ +async function promoteCopy(srcPath, destinationFolder, fileName, sp) { + const { baseURI } = sp.api.file.copy; + const rootFolder = baseURI.split('/').pop(); + + const payload = { ...sp.api.file.copy.payload, parentReference: { path: `${rootFolder}${destinationFolder}` } }; + + const options = await getAuthorizedRequestOption({ + method: sp.api.file.copy.method, + body: JSON.stringify(payload), + }); + + // copy source is the Graybox directory for promote + const copyStatusInfo = await fetchWithRetry(`${sp.api.file.copy.gbBaseURI}${srcPath}:/copy?@microsoft.graph.conflictBehavior=replace`, options); + const statusUrl = copyStatusInfo.headers.get('Location'); + + let copySuccess = false; + let copyStatusJson = {}; + while (statusUrl && !copySuccess && copyStatusJson.status !== 'failed') { + // eslint-disable-next-line no-await-in-loop + const status = await fetchWithRetry(statusUrl); + if (status.ok) { + // eslint-disable-next-line no-await-in-loop + copyStatusJson = await status.json(); + copySuccess = copyStatusJson.status === 'completed'; + } + } + + // If copy failed because it is Locked, try to copy the locked file to a new file, + // then promote copy again, then delete the renamed locked file copy + if (!copySuccess) { + // await releaseUploadSession(sp, uploadFileStatus.sessionUrl); + const lockedFileNewName = getLockedFileNewName(fileName); + // const baseURI = isGraybox ? sp.api.file.get.gbBaseURI : sp.api.file.get.baseURI; + const spFileUrl = `${baseURI}${destinationFolder}/${fileName}`; + await renameFile(spFileUrl, lockedFileNewName); + const folder = getFolderFromPath(`${destinationFolder}/${fileName}`); + const newLockedFilePath = `${folder}/${lockedFileNewName}`; + const copyFileStatus = await copyFile(newLockedFilePath, folder, fileName, false, true, sp); + if (copyFileStatus) { + copySuccess = await promoteCopy(srcPath, destinationFolder, fileName, sp); + if (copySuccess) { + await deleteFile(sp, `${baseURI}${newLockedFilePath}`); + } + } + } + return copySuccess; +} + async function getExcelTable(excelPath, tableName) { const { sp } = await getConfig(); const itemId = await getItemId(sp.api.file.get.baseURI, excelPath); @@ -456,6 +585,7 @@ module.exports = { getFileUsingDownloadUrl, copyFile, saveFile, + promoteCopy, createFolder, updateExcelTable, fetchWithRetry, diff --git a/actions/utils.js b/actions/utils.js index 7aa8ff4..29bb5d6 100644 --- a/actions/utils.js +++ b/actions/utils.js @@ -52,8 +52,8 @@ function isFilePatternMatched(filePath, patterns) { function logMemUsage() { const logger = getAioLogger(); -const memStr = JSON.stringify(process.memoryUsage()); -logger.info(`Memory Usage : ${memStr}`); + const memStr = JSON.stringify(process.memoryUsage()); + logger.info(`Memory Usage : ${memStr}`); } async function delay(milliseconds = 100) { diff --git a/defaultstyles.xml.js b/defaultstyles.xml.js new file mode 100644 index 0000000..aab2f54 --- /dev/null +++ b/defaultstyles.xml.js @@ -0,0 +1,1834 @@ +/* *********************************************************************** + * ADOBE CONFIDENTIAL + * ___________________ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + ************************************************************************* */ + +const DEFAULT_STYLE = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +module.exports = DEFAULT_STYLE; diff --git a/package-lock.json b/package-lock.json index 2e32dd8..0924186 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@adobe/aio-lib-core-logging": "^2.0.0", "@adobe/aio-sdk": "^3.0.0", "@azure/msal-node": "^1.17.2", - "milo-md2docx": "^1.8.1", + "milo-md2docx": "^1.8.0", "milo-parse-markdown": "^1.0.0", "node-fetch": "^2.6.0", "openwhisk": "^3.21.7" diff --git a/package.json b/package.json index 82cc7b9..4a5dc55 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@adobe/aio-lib-core-logging": "^2.0.0", "@adobe/aio-sdk": "^3.0.0", "@azure/msal-node": "^1.17.2", - "milo-md2docx": "^1.8.1", + "milo-md2docx": "^1.8.0", "milo-parse-markdown": "^1.0.0", "node-fetch": "^2.6.0", "openwhisk": "^3.21.7"