UNPKG

salesforce-alm

Version:

This package contains tools, and APIs, for an improved salesforce.com developer experience.

374 lines (372 loc) 17.3 kB
"use strict"; /* * Copyright (c) 2020, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ Object.defineProperty(exports, "__esModule", { value: true }); exports.StaticResource = void 0; const process = require("process"); const os = require("os"); const path = require("path"); const util = require("util"); const fs = require("fs-extra"); const core_1 = require("@salesforce/core"); const mime = require("mime"); const AdmZip = require("adm-zip"); const BBPromise = require("bluebird"); const core_2 = require("@salesforce/core"); const srcDevUtil = require("../../core/srcDevUtil"); const xmlMetadataDocument_1 = require("../xmlMetadataDocument"); const PathUtil = require("../sourcePathUtil"); const sourceUtil_1 = require("../sourceUtil"); const mimeTypeExtensions = require('mime/types.json'); const fallBackMimeTypeExtensions = require('../mimeTypes'); /** * Encapsulates logic for handling of static resources. * * Static resources differ in the following ways from default mdapi expectations: * 1. The file name has a normal extension reflecting the mime type (zip, jar, jpeg) rather than "resource" * 2. A zip or jar archive can be exploded into a directory, and will be by default on pull. Only if an * archive file with the resource full name exists in the resources directory will it remain zipped. * * Note that when an archive is expanded on pull no attempt is made to avoid redundant updates of unmodified files * (as would happen in some other metadata decompositions). */ class StaticResource { constructor(metadataPath, metadataType, workspaceVersion, retrievedMetadataFilePath, unsupportedMimeTypes) { this.metadataPath = metadataPath; this.metadataType = metadataType; this.usingGAWorkspace = !util.isNullOrUndefined(workspaceVersion); // TODO - once we know what the version looks like this.resourcesDir = path.dirname(metadataPath); this.fullName = this.metadataType.getFullNameFromFilePath(metadataPath); const effectiveMetadataFilePath = util.isNullOrUndefined(retrievedMetadataFilePath) ? metadataPath : retrievedMetadataFilePath; this.mimeType = StaticResource.getMimeType(effectiveMetadataFilePath); this.fileExtensions = this.getMimeTypeExtension(unsupportedMimeTypes); this.multiVersionHackUntilWorkspaceVersionsAreSupported = true; // sigh } getResource() { if (this.multiVersionHackUntilWorkspaceVersionsAreSupported) { if (this.isExplodedArchive()) { return StaticResource.zipDir(this.getExplodedFolderPath()); } else if (srcDevUtil.pathExistsSync(this.getLegacyFilePath())) { return BBPromise.resolve(this.getLegacyFilePath()); } else if (srcDevUtil.pathExistsSync(this.getSingleFilePathPreferExisting())) { return BBPromise.resolve(this.getSingleFilePathPreferExisting()); } else { return BBPromise.resolve(PathUtil.getContentPathWithNonStdExtFromMetadataPath(this.metadataPath)); } } else { if (this.usingGAWorkspace) { if (this.isExplodedArchive()) { return StaticResource.zipDir(this.getExplodedFolderPath()); } else { return BBPromise.resolve(this.getSingleFilePathPreferExisting()); } } else { return BBPromise.resolve(this.getLegacyFilePath()); } } } async saveResource(sourcePath, createDuplicates, forceoverwrite = false) { const updatedPaths = []; const duplicatePaths = []; const deletedPaths = []; if (this.multiVersionHackUntilWorkspaceVersionsAreSupported) { if (this.isExplodedArchive()) { return this.expandArchive(sourcePath, createDuplicates, forceoverwrite); } else if (srcDevUtil.pathExistsSync(this.getLegacyFilePath())) { return await this.handleLegacyPath(sourcePath, createDuplicates); } else { return await this.handleResource(sourcePath, createDuplicates, forceoverwrite); } } else { if (this.usingGAWorkspace) { if (this.isExplodedArchive()) { return this.expandArchive(sourcePath, createDuplicates, forceoverwrite); } else { await fs.copyFile(sourcePath, this.getSingleFilePathPreferExisting()); updatedPaths.push(this.getSingleFilePathPreferExisting()); } } else { return await this.handleResource(sourcePath, createDuplicates, forceoverwrite); } } return [updatedPaths, duplicatePaths, deletedPaths]; } isExplodedArchive() { if (this.multiVersionHackUntilWorkspaceVersionsAreSupported) { const singleFileArchiveExists = srcDevUtil.pathExistsSync(this.getSingleFilePath()) || srcDevUtil.pathExistsSync(this.getLegacyFilePath()); return this.isArchiveMimeType() && !singleFileArchiveExists; } else { const singleFileArchiveExists = srcDevUtil.pathExistsSync(this.getSingleFilePath()); return this.isArchiveMimeType() && !singleFileArchiveExists; } } getContentPaths() { let contentPaths = []; if (this.multiVersionHackUntilWorkspaceVersionsAreSupported) { if (this.isExplodedArchive()) { if (srcDevUtil.pathExistsSync(this.getExplodedFolderPath())) { contentPaths = contentPaths.concat(this.getFiles(this.getExplodedFolderPath())); } } else if (srcDevUtil.pathExistsSync(this.getLegacyFilePath())) { contentPaths.push(this.getLegacyFilePath()); } else { const contentPath = this.getSingleFilePathPreferExisting(); if (srcDevUtil.pathExistsSync(contentPath)) { contentPaths.push(contentPath); } } } else { if (this.usingGAWorkspace) { if (this.isExplodedArchive()) { if (srcDevUtil.pathExistsSync(this.getExplodedFolderPath())) { contentPaths = contentPaths.concat(this.getFiles(this.getExplodedFolderPath())); } } else { const contentPath = this.getSingleFilePathPreferExisting(); if (srcDevUtil.pathExistsSync(contentPath)) { contentPaths.push(contentPath); } } } else if (srcDevUtil.pathExistsSync(this.getLegacyFilePath())) { contentPaths.push(this.getLegacyFilePath()); } } return contentPaths; } static zipDir(dir) { const zipFile = path.join(os.tmpdir() || '.', `sdx_srzip_${process.hrtime()[0]}${process.hrtime()[1]}.zip`); return srcDevUtil.zipDir(dir, zipFile, { level: 9 }); } /** * Get the mime type from the npm mime library file. * If the mime type is not supported there, use our backup manually added mime types file * If the mime type is not supported there, throw an error * * @param unsupportedMimeTypes - an array of unsupported mime types for the purpose of logging * @returns {string[]} the mime type extension(s) */ getMimeTypeExtension(unsupportedMimeTypes) { let ext = mimeTypeExtensions[this.mimeType]; if (!ext || ext.length === 0) { ext = fallBackMimeTypeExtensions[this.mimeType]; } if (ext) { return ext; } if (unsupportedMimeTypes) { unsupportedMimeTypes.push(this.mimeType); } return [this.metadataType.getExt()]; } getFiles(file) { let found = []; found.push(file); const stat = fs.statSync(file); if (stat.isDirectory()) { const nestedFiles = fs.readdirSync(file); nestedFiles.forEach((nestedFile) => { const nestedPath = path.join(file, nestedFile); const nestedStat = fs.statSync(nestedPath); if (nestedStat.isDirectory()) { found = found.concat(this.getFiles(nestedPath)); } else { found.push(nestedPath); } }); } return found; } static getMimeType(metadataPath) { if (srcDevUtil.pathExistsSync(metadataPath)) { const doc = new xmlMetadataDocument_1.XmlMetadataDocument('StaticResource'); try { doc.setRepresentation(fs.readFileSync(metadataPath, 'utf8')); } catch (e) { throw sourceUtil_1.checkForXmlParseError(metadataPath, e); } const nodeTypeElement = 1; let child = doc.data.documentElement.firstChild; while (child !== null) { if (child.nodeType === nodeTypeElement && child.nodeName === 'contentType') { return child.firstChild.nodeValue; } child = child.nextSibling; } } return mime.lookup(''); // this defaults to bin -- is this correct? } getLegacyFilePath() { return path.join(this.resourcesDir, `${this.fullName}.${this.metadataType.getExt()}`); } getSingleFilePath(ext) { let extension = ext; if (!ext) { const mimeLib = mime.extension(this.mimeType); extension = mimeLib ? mimeLib : this.getMimeTypeExtension(); } return path.join(this.resourcesDir, `${this.fullName}.${extension}`); } getSingleFilePathPreferExisting() { // eslint-disable-next-line @typescript-eslint/no-shadow const ext = this.fileExtensions.find((ext) => srcDevUtil.pathExistsSync(this.getSingleFilePath(ext))); return this.getSingleFilePath(ext); } getExplodedFolderPath() { return path.join(this.resourcesDir, this.fullName); } isArchiveMimeType() { const fallBackExtension = fallBackMimeTypeExtensions[this.mimeType]; let isZip = false; if (fallBackExtension) { isZip = fallBackExtension[0] === 'zip'; } return this.mimeType === mime.lookup('zip') || this.mimeType === mime.lookup('jar') || isZip; } async expandArchive(sourcePath, createDuplicates, forceoverwrite = false) { const updatedPaths = []; const duplicatePaths = []; // expand the archive into a temp directory const tempDir = path.join(os.tmpdir(), `sfdx_staticresource_${this.fullName}_${Date.now()}`); srcDevUtil.ensureDirectoryExistsSync(tempDir); try { new AdmZip(sourcePath).extractAllTo(tempDir); } catch (error) { throw core_2.SfdxError.create('salesforce-alm', 'mdapi_convert', 'AdmZipError', [sourcePath, this.mimeType, error]) .message; } // compare exploded directories if needed const isUpdatingExistingStaticResource = srcDevUtil.pathExistsSync(this.getExplodedFolderPath()); if (isUpdatingExistingStaticResource && createDuplicates) { await this.compareExplodedDirs(tempDir, duplicatePaths, updatedPaths, forceoverwrite); } const existingPaths = new Set(); if (isUpdatingExistingStaticResource) { // eslint-disable-next-line @typescript-eslint/require-await await core_1.fs.actOn(this.getExplodedFolderPath(), async (file) => { existingPaths.add(file); }); } // now copy all the files in the temp dir into the workspace srcDevUtil.deleteDirIfExistsSync(this.getExplodedFolderPath()); fs.copySync(tempDir, this.getExplodedFolderPath()); // override new file with existing // if this is a new static resource then simply report all files as changed if (!isUpdatingExistingStaticResource || forceoverwrite) { // eslint-disable-next-line @typescript-eslint/require-await await core_1.fs.actOn(tempDir, async (file) => { updatedPaths.push(file.replace(tempDir, this.getExplodedFolderPath())); }); } else { // if this is an existing static resource then we want to figure which files are new // and add those to updatedPaths // eslint-disable-next-line @typescript-eslint/require-await await core_1.fs.actOn(tempDir, async (file) => { const filePath = file.replace(tempDir, this.getExplodedFolderPath()); if (!existingPaths.has(filePath)) { updatedPaths.push(filePath); } existingPaths.delete(filePath); }); } // We determine the deleted paths by removing files from existingPaths as they're found in the tempDir // whatever remains we assume needs to be deleted const deletedPaths = existingPaths.size ? [...existingPaths] : []; return [updatedPaths, duplicatePaths, deletedPaths]; } // if an exploded directory structure exists in the workspace then loop through each new file and see if a file // with same name exists in the workspace. If that file exists then compare the hashes. If hashes are different // then create a duplicate file. async compareExplodedDirs(tempDir, duplicatePaths, updatedPaths, forceoverwrite = false) { await core_1.fs.actOn(tempDir, async (file) => { if (!fs.statSync(file).isDirectory()) { const relativePath = file.substring(file.indexOf(tempDir) + tempDir.length); const workspaceFile = path.join(this.getExplodedFolderPath(), relativePath); if (srcDevUtil.pathExistsSync(workspaceFile)) { const equalCheck = await core_1.fs.areFilesEqual(workspaceFile, file); if (!equalCheck) { // file with same name exists and contents are different await fs.copyFile(file, file + '.dup'); // copy newFile to .dup duplicatePaths.push(workspaceFile + '.dup'); // keep track of dups await fs.copyFile(workspaceFile, file); } else if (forceoverwrite) { // if file exists and contents are the same then don't report it as updated unless we're force overwriting updatedPaths.push(workspaceFile); // override new file with existing } } else { updatedPaths.push(workspaceFile); // this is a net new file } } }); } async handleResource(sourcePath, createDuplicates, forceoverwrite = false) { const updatedPaths = []; const duplicatePaths = []; const deletedPaths = []; const destFile = this.getSingleFilePathPreferExisting(); if (forceoverwrite || !(await core_1.fs.fileExists(destFile))) { await fs.copyFile(sourcePath, destFile); updatedPaths.push(this.getSingleFilePathPreferExisting()); } else { const compareFiles = await core_1.fs.areFilesEqual(sourcePath, destFile); if (!compareFiles && createDuplicates) { await fs.copyFile(sourcePath, `${this.getSingleFilePathPreferExisting()}.dup`); duplicatePaths.push(`${destFile}.dup`); } else if (!compareFiles && !createDuplicates) { // replace existing file with remote await fs.copyFile(sourcePath, destFile); duplicatePaths.push(this.getSingleFilePathPreferExisting()); } } return [updatedPaths, duplicatePaths, deletedPaths]; } async handleLegacyPath(sourcePath, createDuplicates) { const updatedPaths = []; const duplicatePaths = []; const deletedPaths = []; const legacyFilePath = this.getLegacyFilePath(); if (createDuplicates) { if (!(await core_1.fs.areFilesEqual(sourcePath, legacyFilePath))) { await fs.copyFile(sourcePath, legacyFilePath + '.dup'); duplicatePaths.push(legacyFilePath + '.dup'); } // if contents are equal and we are doing a mdapi:convert (createDuplicates=true) then ignore this file } else { await fs.copyFile(sourcePath, legacyFilePath); updatedPaths.push(legacyFilePath); } return [updatedPaths, duplicatePaths, deletedPaths]; } } exports.StaticResource = StaticResource; //# sourceMappingURL=staticResource.js.map