From 0677ba95666f1208c1850f774b1e3880a927fff9 Mon Sep 17 00:00:00 2001 From: Robert Patrick Date: Fri, 13 May 2022 10:49:17 -0500 Subject: [PATCH 1/2] adding upfront check to make sure the user has write permission on the selected directory for the project file --- electron/app/js/fsUtils.js | 19 +++++++ electron/app/js/project.js | 57 ++++++++++++-------- electron/app/locales/en/electron.json | 5 ++ webui/src/js/viewModels/model-design-view.js | 4 +- 4 files changed, 61 insertions(+), 24 deletions(-) diff --git a/electron/app/js/fsUtils.js b/electron/app/js/fsUtils.js index 5a2f58823..ef80b29c4 100644 --- a/electron/app/js/fsUtils.js +++ b/electron/app/js/fsUtils.js @@ -3,12 +3,14 @@ * Copyright (c) 2021, 2022, Oracle and/or its affiliates. * Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. */ +const { constants } = require('fs'); const fsPromises = require('fs/promises'); const path = require('path'); const osUtils = require('./osUtils'); const which = require('which'); const { getErrorMessage } = require('./errorUtils'); +const errorUtils = require('./errorUtils'); // WARNING: This file is used early on in the startup process so do not require other modules at the top level // that depend on Electron being fully initialized. For example, i18next.config... @@ -292,6 +294,22 @@ async function getDirectoryForPath(fileSystemPath) { }); } +async function canWriteInDirectory(filePath) { + return new Promise(resolve => { + isDirectory(filePath).then(isDir => { + let pathToCheck = filePath; + if (!isDir) { + pathToCheck = path.dirname(filePath); + } + fsPromises.access(pathToCheck, constants.R_OK | constants.W_OK).then(() => { + resolve(true); + }).catch(() => { + resolve(false); + }); + }); + }); +} + async function _getFilesRecursivelyFromDirectory(directory, fileList) { const i18n = require('./i18next.config'); @@ -331,6 +349,7 @@ async function _processDirectoryListing(directory, listing, fileList) { } module.exports = { + canWriteInDirectory, createTemporaryDirectory, exists, getAbsolutePath, diff --git a/electron/app/js/project.js b/electron/app/js/project.js index 8bbb2ed3c..589c62aca 100644 --- a/electron/app/js/project.js +++ b/electron/app/js/project.js @@ -45,24 +45,18 @@ function showExistingProjectWindow(existingProjectWindow) { } async function createNewProject(targetWindow) { - const saveResponse = await dialog.showSaveDialog(targetWindow, { - title: 'Create WebLogic Kubernetes Toolkit Project', - buttonLabel: 'Create Project', - filters: [ - { name: i18n.t(projectFileTypeKey), extensions: [projectFileExtension] } - ], - properties: [ - 'createDirectory', - 'showOverwriteConfirmation' - ] + const titleKey = 'dialog-createNewProjectTitle'; + const buttonKey = 'button-create'; + + return new Promise(resolve => { + _chooseProjectSaveFile(targetWindow,titleKey, buttonKey).then(projectFileName => { + if (projectFileName) { + sendToWindow(targetWindow, 'start-new-project', projectFileName); + // window will reply with new-project -> initializeNewProject() including isDirty flag + } + resolve(); + }); }); - - if (saveResponse.canceled || !saveResponse.filePath || projectFileAlreadyOpen(saveResponse.filePath)) { - return; - } - - sendToWindow(targetWindow, 'start-new-project', saveResponse.filePath); - // window will reply with new-project -> initializeNewProject() including isDirty flag } async function initializeNewProject(targetWindow, projectFile, isDirty) { @@ -145,7 +139,10 @@ async function confirmProjectFile(targetWindow) { projectFile = await _chooseProjectSaveFile(targetWindow); projectName = projectFile ? _generateProjectName(projectFile) : null; projectUuid = projectFile ? _generateProjectUuid() : null; - app.addRecentDocument(projectFile); + if (projectFile) { + getLogger().debug('confirmProjectFile adding %s to recent documents', projectFile); + app.addRecentDocument(projectFile); + } } return [projectFile, projectName, projectUuid]; } @@ -157,7 +154,10 @@ async function chooseProjectFile(targetWindow) { const projectFile = await _chooseProjectSaveFile(targetWindow); const projectName = projectFile ? _generateProjectName(projectFile) : null; const projectUuid = projectFile ? _generateProjectUuid() : null; - app.addRecentDocument(projectFile); + if (projectFile) { + getLogger().debug('chooseProjectFile adding %s to recent documents', projectFile); + app.addRecentDocument(projectFile); + } return [projectFile, projectName, projectUuid]; } @@ -306,6 +306,7 @@ async function exportArchiveFile(targetWindow, archivePath, projectFile) { // Private helper methods // async function _createNewProjectFile(targetWindow, projectFileName) { + getLogger().debug('entering _createNewProjectFile() for %s', projectFileName); return new Promise((resolve) => { const projectContents = _addProjectIdentifiers(projectFileName, emptyProjectContents); const projectContentsJson = JSON.stringify(projectContents, null, 2); @@ -381,6 +382,7 @@ async function _openProjectFile(targetWindow, projectFileName) { const wktWindow = require('./wktWindow'); wktWindow.setTitleFileName(targetWindow, projectFileName, false); targetWindow.setRepresentedFilename(projectFileName); + getLogger().debug('_openProjectFile adding %s to recent documents', projectFileName); app.addRecentDocument(projectFileName); resolve(); }).catch(err => reject(err)); @@ -513,8 +515,8 @@ async function _sendProjectOpened(targetWindow, file, jsonContents) { sendToWindow(targetWindow, 'project-opened', file, jsonContents, modelFilesContentJson); } -async function _chooseProjectSaveFile(targetWindow) { - const title = i18n.t('dialog-chooseProjectSaveFile'); +async function _chooseProjectSaveFile(targetWindow, titleKey = 'dialog-chooseProjectSaveFile', buttonKey = 'button-save') { + const title = i18n.t(titleKey); let saveResponse = await dialog.showSaveDialog(targetWindow, { title: title, @@ -522,7 +524,7 @@ async function _chooseProjectSaveFile(targetWindow) { filters: [ {name: i18n.t(projectFileTypeKey), extensions: [projectFileExtension]} ], - buttonLabel: i18n.t('button-save'), + buttonLabel: i18n.t(buttonKey), properties: [ 'createDirectory', 'showOverwriteConfirmation' @@ -532,6 +534,17 @@ async function _chooseProjectSaveFile(targetWindow) { if (saveResponse.canceled || !saveResponse.filePath || projectFileAlreadyOpen(saveResponse.filePath)) { return null; } + + // Do a quick sanity check to make sure that the user has permissions to + // write to the directory chosen. If not, show them the error and return null. + // + if (! await fsUtils.canWriteInDirectory(saveResponse.filePath)) { + const errTitle = i18n.t('dialog-projectSaveFileLocationNotWritableTitle'); + const errMessage = i18n.t('dialog-projectSaveFileLocationNotWritableError', + { projectFileDirectory: path.dirname(saveResponse.filePath)}); + dialog.showErrorBox(errTitle, errMessage); + return null; + } return getProjectFileName(saveResponse.filePath); } diff --git a/electron/app/locales/en/electron.json b/electron/app/locales/en/electron.json index 733a24952..63f56efbf 100644 --- a/electron/app/locales/en/electron.json +++ b/electron/app/locales/en/electron.json @@ -138,6 +138,10 @@ "dialog-openProjectFileParseErrorMessage": "Unable to read the project file {{projectFileName}} as JSON: {{err}}", "dialog-openProjectFileReadErrorTitle": "Failed to Read Project File", "dialog-openProjectFileReadErrorMessage": "Unable to read the project file {{projectFileName}}: {{err}}", + "dialog-createNewProjectTitle": "Create WebLogic Kubernetes Toolkit Project", + "dialog-projectSaveFileLocationNotWritableTitle": "Project Directory Not Writable", + "dialog-projectSaveFileLocationNotWritableError": "The {{projectFileDirectory}} directory chosen to write the new project file is not writable by the current process.", + "dialog-openProjectWindowPrompt": "Choose the window where the project should be opened.", "dialog-chooseDomainHome": "Select the Domain Home directory to use", "dialog-chooseJavaHome": "Select the Java Home directory to use", @@ -189,6 +193,7 @@ "button-select": "Select", "button-cancel": "Cancel", "button-save": "Save", + "button-create": "Create", "button-ok": "OK", "button-yes": "Yes", "button-openProject": "Open Project", diff --git a/webui/src/js/viewModels/model-design-view.js b/webui/src/js/viewModels/model-design-view.js index 5a7c701fe..bd4255ceb 100644 --- a/webui/src/js/viewModels/model-design-view.js +++ b/webui/src/js/viewModels/model-design-view.js @@ -164,7 +164,7 @@ function(accUtils, i18n, ko, project, urlCatalog, viewHelper, wktLogger, ViewMod // this.providerActivated = (event) => { this.dataProvider = event.detail.value; - wktLogger.debug('Received providerActivated event with dataProvider = %s', JSON.stringify(this.dataProvider)); + wktLogger.debug('Received providerActivated event with dataProvider'); this.designer.selectLastVisitedSlice(); }; @@ -194,7 +194,7 @@ function(accUtils, i18n, ko, project, urlCatalog, viewHelper, wktLogger, ViewMod // this.providerDeactivated = (event) => { const result = event.detail.value; - wktLogger.debug('Received providerDeactivated event with dataProvider = %s', JSON.stringify(result)); + wktLogger.debug('Received providerDeactivated event with dataProvider'); delete result.data; this.dataProvider = {state: 'disconnected'}; }; From 22f63217ed95f2b59ee917dd8b2fd92b04ba0b8e Mon Sep 17 00:00:00 2001 From: Robert Patrick Date: Fri, 13 May 2022 16:33:12 -0500 Subject: [PATCH 2/2] catching and handling writeFile errors when saving the project --- electron/app/js/fsUtils.js | 1 - electron/app/js/modelArchive.js | 7 +- electron/app/js/project.js | 137 ++++++++++++++++++++----- electron/app/locales/en/electron.json | 2 + electron/app/main.js | 4 +- webui/src/js/utils/project-io.js | 32 +++--- webui/src/js/utils/wkt-actions-base.js | 3 +- 7 files changed, 141 insertions(+), 45 deletions(-) diff --git a/electron/app/js/fsUtils.js b/electron/app/js/fsUtils.js index ef80b29c4..b5891480b 100644 --- a/electron/app/js/fsUtils.js +++ b/electron/app/js/fsUtils.js @@ -10,7 +10,6 @@ const osUtils = require('./osUtils'); const which = require('which'); const { getErrorMessage } = require('./errorUtils'); -const errorUtils = require('./errorUtils'); // WARNING: This file is used early on in the startup process so do not require other modules at the top level // that depend on Electron being fully initialized. For example, i18next.config... diff --git a/electron/app/js/modelArchive.js b/electron/app/js/modelArchive.js index af07a7f47..26771b745 100644 --- a/electron/app/js/modelArchive.js +++ b/electron/app/js/modelArchive.js @@ -355,11 +355,16 @@ async function _addDirectoryToArchiveFile(archiveFile, zip, zipPath, dirPath) { } async function _removePathFromArchive(archiveFile, zip, zipPath) { + if (!zipPath) { + getLogger().warn('_removePathFromArchive received empty zipPath so skipping...'); + return Promise.resolve(); + } + return new Promise((resolve, reject) => { try { if (zip.file(zipPath)) { zip.remove(zipPath); - } else if (zipPath.endsWith('/')) { + } else if (zipPath && zipPath.endsWith('/')) { // Remove the trailing slash so the target folder is also removed, not just its contents... zip.remove(zipPath.slice(0, -1)); } diff --git a/electron/app/js/project.js b/electron/app/js/project.js index 07d67d48e..8dc532be9 100644 --- a/electron/app/js/project.js +++ b/electron/app/js/project.js @@ -17,6 +17,7 @@ const { getLogger } = require('./wktLogging'); const { sendToWindow } = require('./windowUtils'); const i18n = require('./i18next.config'); const { CredentialStoreManager, EncryptedCredentialManager, CredentialNoStoreManager } = require('./credentialManager'); +const errorUtils = require('./errorUtils'); const projectFileTypeKey = 'dialog-wktFileType'; const projectFileExtension = 'wktproj'; @@ -24,6 +25,39 @@ const emptyProjectContents = {}; const openProjects = new Map(); +// This file is the central file controlling all the project create and save functionality. +// As such, there are a number of flows that make 1 or more calls into the methods in this file. +// +// Create New Project menu item flow: +// - The entry point is createNewProject(): +// + asks the user for the project file location +// + sends the start-new-project message to the renderer +// - On receiving the start-new-project message: +// + the renderer gathers any project-related state +// + sends the new-project message +// - The new-project message calls initializeNewProject(): +// + writes the file +// + handles other necessary state management if the file write succeeds +// +// Save All menu item flow: +// - The entry point is startSaveProject(): +// + sends the start-save-project message to renderer +// - On receiving the start-save-project message: +// + determines if the project needs saving +// + invokes confirm-project-file +// - The confirm-project-file calls confirmProjectFile(): +// + gets the project file name, prompting the user if required +// + returns the name, uuid, and file name (or null if file is not selected) +// - On receiving the response to the confirm-project-file invocation, the renderer: +// + gathers the project-related data +// + invokes save-project +// - The save-project calls saveProject() +// + saves the project file +// + if project file save succeeds, saves any model files +// + returns whether the save was successful and the model file contents +// - On receiving the response to the save-project invocation, the renderer ends the flow +// + // Public methods // function isWktProjectFile(filename) { @@ -66,21 +100,21 @@ async function initializeNewProject(targetWindow, projectFile, isDirty) { return; } - let projectFileName = getProjectFileName(projectFile); - if (path.extname(projectFileName) !== `.${projectFileExtension}`) { - projectFileName = `${projectFileName}.${projectFileExtension}`; - } + const projectFileName = getProjectFileName(projectFile); const wktWindow = require('./wktWindow'); - if(projectWindow.id === targetWindow.id) { - wktWindow.setTitleFileName(projectWindow, projectFileName, false); - } else { - projectWindow.on('ready-to-show', () => { + + const wroteFile = await _createNewProjectFile(projectWindow, projectFileName); + if (wroteFile) { + if (projectWindow.id === targetWindow.id) { wktWindow.setTitleFileName(projectWindow, projectFileName, false); - }); + } else { + projectWindow.on('ready-to-show', () => { + wktWindow.setTitleFileName(projectWindow, projectFileName, false); + }); + } + app.addRecentDocument(projectFileName); + projectWindow.setRepresentedFilename(projectFileName); } - await _createNewProjectFile(projectWindow, projectFileName); - app.addRecentDocument(projectFileName); - projectWindow.setRepresentedFilename(projectFileName); } async function openProject(targetWindow) { @@ -126,7 +160,7 @@ async function openProjectFile(targetWindow, projectFile, isDirty) { .catch(err => { dialog.showErrorBox( i18n.t('dialog-openProjectFileErrorTitle'), - i18n.t('dialog-openProjectFileErrorMessage', { projectFileName: projectFile, err: err }), + i18n.t('dialog-openProjectFileErrorMessage', { projectFileName: projectFile, err: errorUtils.getErrorMessage(err) }), ); getLogger().error('Failed to open project file %s: %s', projectFile, err); }); @@ -179,15 +213,43 @@ function startSaveProjectAs(targetWindow) { // save the specified project and model contents to the project file. // usually invoked by the save-project IPC invocation. -async function saveProject(targetWindow, projectFile, projectContents, externalFileContents) { +async function saveProject(targetWindow, projectFile, projectContents, externalFileContents, showErrors = true) { // the result will contain only sections that were updated due to save, such as model.archiveFiles - const saveResult = {}; + const saveResult = { + isProjectFileSaved: false, + areModelFilesSaved: false + }; - _assignProjectFile(targetWindow, projectFile); - saveResult['model'] = await _saveExternalFileContents(_getProjectDirectory(targetWindow), externalFileContents); - await _saveProjectFile(targetWindow, projectFile, projectContents); - const wktWindow = require('./wktWindow'); - wktWindow.setTitleFileName(targetWindow, projectFile, false); + const assignProjectFileData = _assignProjectFile(targetWindow, projectFile); + try { + await _saveProjectFile(targetWindow, projectFile, projectContents); + saveResult.isProjectFileSaved = true; + } catch (err) { + if (showErrors) { + _showSaveError(projectFile, err); + } + getLogger().error('Failed to save project file %s: %s', projectFile, err); + // revert the project assignment to the window + _revertAssignProjectFile(assignProjectFileData); + saveResult.reason = i18n.t('dialog-saveProjectFileErrorMessage', { projectFileName: projectFile, err: err }); + } + + if (saveResult.isProjectFileSaved) { + const wktWindow = require('./wktWindow'); + wktWindow.setTitleFileName(targetWindow, projectFile, false); + try { + saveResult['model'] = await _saveExternalFileContents(_getProjectDirectory(targetWindow), externalFileContents); + saveResult.areModelFilesSaved = true; + } catch (err) { + const message = i18n.t('project-save-model-files-error-message', { error: errorUtils.getErrorMessage(err) }); + if (showErrors) { + const title = i18n.t('project-save-model-files-error-title'); + dialog.showErrorBox(title, message); + } + getLogger().error('Failed to save one of the model files for project file %s: %s', projectFile, err); + saveResult.reason = message; + } + } return saveResult; } @@ -318,19 +380,23 @@ async function _createNewProjectFile(targetWindow, projectFileName) { .then(() => { _addOpenProject(targetWindow, projectFileName, false, new CredentialStoreManager(projectContents.uuid)); sendToWindow(targetWindow, 'project-created', projectFileName, projectContents); - resolve(); + resolve(true); }) .catch(err => { - dialog.showErrorBox( - i18n.t('dialog-saveProjectFileErrorTitle'), - i18n.t('dialog-saveProjectFileErrorMessage', { projectFileName: projectFileName, err: err }), - ); + _showSaveError(projectFileName, err); getLogger().error('Failed to save new project in file %s: %s', projectFileName, err); - resolve(); + resolve(false); }); }); } +function _showSaveError(projectFileName, err) { + dialog.showErrorBox( + i18n.t('dialog-saveProjectFileErrorTitle'), + i18n.t('dialog-saveProjectFileErrorMessage', { projectFileName: projectFileName, err: err }), + ); +} + function _addProjectIdentifiers(projectFileName, projectContents) { const alreadyHasName = Object.prototype.hasOwnProperty.call(projectContents, 'name'); const alreadyHasGuid = Object.prototype.hasOwnProperty.call(projectContents, 'uuid'); @@ -764,6 +830,21 @@ function _assignProjectFile(targetWindow, projectFile) { if (newFile !== oldFile) { _addOpenProject(targetWindow, projectFile, false); } + return { + existingProject, + oldFile, + newFile + }; +} + +function _revertAssignProjectFile(targetWindow, assignProjectFileData) { + if (assignProjectFileData.existingProject) { + if (assignProjectFileData.oldFile !== assignProjectFileData.newFile) { + _addOpenProject(targetWindow, assignProjectFileData.oldFile, false); + } + } else { + openProjects.delete(targetWindow); + } } async function _createCredentialManager(targetWindow, projectFileJsonContent) { @@ -875,8 +956,8 @@ function _setCredentialManager(targetWindow, credentialManager) { // On Linux, the save dialog does not automatically add the project file extension... function getProjectFileName(dialogReturnedFileName) { let result = dialogReturnedFileName; - if (dialogReturnedFileName && path.extname(dialogReturnedFileName) !== '.wktproj') { - result = `${dialogReturnedFileName}.wktproj`; + if (dialogReturnedFileName && path.extname(dialogReturnedFileName) !== `.${projectFileExtension}`) { + result = `${dialogReturnedFileName}.${projectFileExtension}`; } return result; } diff --git a/electron/app/locales/en/electron.json b/electron/app/locales/en/electron.json index 63f56efbf..66653ecf0 100644 --- a/electron/app/locales/en/electron.json +++ b/electron/app/locales/en/electron.json @@ -376,6 +376,8 @@ "project-save-file-already-open-title": "Project File Already Open", "project-save-file-already-open-message": "The project file {{projectFile}} is already open in another window. Please either close the existing window or select a different project file and try again.", + "project-save-model-files-error-title": "Project Model Files Save Error", + "project-save-model-files-error-message": "Failed to save one of the WebLogic Deploy Tooling model files associated with the project: {{error}}", "wrc-home-error-title": "Invalid WebLogic Remote Console Location", "wrc-init-error-title": "Initializing WebLogic Remote Console Backend Failed", diff --git a/electron/app/main.js b/electron/app/main.js index 001a79e33..8f8bbd12d 100644 --- a/electron/app/main.js +++ b/electron/app/main.js @@ -616,8 +616,8 @@ class Main { }); ipcMain.handle('save-project',async (event, projectFile, projectContents, - externalFileContents) => { - return project.saveProject(event.sender.getOwnerBrowserWindow(), projectFile, projectContents, externalFileContents); + externalFileContents, displayElectronSideErrors = true) => { + return project.saveProject(event.sender.getOwnerBrowserWindow(), projectFile, projectContents, externalFileContents, displayElectronSideErrors); }); ipcMain.handle('close-project', async (event, keepWindow) => { diff --git a/webui/src/js/utils/project-io.js b/webui/src/js/utils/project-io.js index 5132e843f..d69931f39 100644 --- a/webui/src/js/utils/project-io.js +++ b/webui/src/js/utils/project-io.js @@ -16,7 +16,7 @@ define(['knockout', 'models/wkt-project', 'utils/i18n'], // verify that a project file is assigned to this project, choosing if necessary. // save the project contents to the specified file. - this.saveProject = async(forceSave) => { + this.saveProject = async(forceSave = false, displayElectronSideErrors = true) => { const projectNotSaved = !project.getProjectFileName(); if(forceSave || project.isDirty() || projectNotSaved) { @@ -27,7 +27,7 @@ define(['knockout', 'models/wkt-project', 'utils/i18n'], return {saved: false, reason: i18n.t('project-io-user-cancelled-save-message')}; } - return saveToFile(projectFile, projectName, projectUuid); + return saveToFile(projectFile, projectName, projectUuid, displayElectronSideErrors); } return {saved: true}; @@ -57,10 +57,8 @@ define(['knockout', 'models/wkt-project', 'utils/i18n'], // if project file is null, do not save. // if project name and UUID are specified, this is a new file, so assign those. // return object with saved status and reason if not saved. - async function saveToFile(projectFile, projectName, projectUuid) { - - project.setProjectFileName(projectFile); - + async function saveToFile(projectFile, projectName, projectUuid, displayElectronSideErrors = true) { + const result = { saved: true }; // if project name or UUID are null, they were previously assigned. if(projectName) { project.setProjectName(projectName); @@ -72,14 +70,24 @@ define(['knockout', 'models/wkt-project', 'utils/i18n'], let projectContents = project.getProjectContents(); let modelContents = project.wdtModel.getModelContents(); const saveResult = await window.api.ipc.invoke('save-project', projectFile, projectContents, - modelContents); + modelContents, displayElectronSideErrors); - if(saveResult['model']) { - project.wdtModel.setSpecifiedModelFiles(saveResult['model']); + if (saveResult['isProjectFileSaved']) { + project.setProjectFileName(projectFile); + if (saveResult['areModelFilesSaved']) { + if(saveResult['model']) { + project.wdtModel.setSpecifiedModelFiles(saveResult['model']); + } + project.setNotDirty(); + } else { + result['saved'] = false; + result['reason'] = saveResult.reason; + } + } else { + result['saved'] = false; + result['reason'] = saveResult.reason; } - - project.setNotDirty(); - return {saved: true}; + return result; } // close the project in this window. diff --git a/webui/src/js/utils/wkt-actions-base.js b/webui/src/js/utils/wkt-actions-base.js index 53df8b978..64b741524 100644 --- a/webui/src/js/utils/wkt-actions-base.js +++ b/webui/src/js/utils/wkt-actions-base.js @@ -92,7 +92,8 @@ function(project, wktConsole, i18n, projectIo, dialogHelper, async saveProject(errTitle, errPrefix, shouldCloseBusyDialog = true) { try { - const saveResult = await projectIo.saveProject(); + + const saveResult = await projectIo.saveProject(false, false); if (!saveResult.saved) { const errKey = `${errPrefix}-project-not-saved-error-prefix`; const errMessage = `${i18n.t(errKey)}: ${saveResult.reason}`;