Skip to content

Fixing issues when saving the project encounters errors #131

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions electron/app/js/fsUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* 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');
Expand Down Expand Up @@ -292,6 +293,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');

Expand Down Expand Up @@ -331,6 +348,7 @@ async function _processDirectoryListing(directory, listing, fileList) {
}

module.exports = {
canWriteInDirectory,
createTemporaryDirectory,
exists,
getAbsolutePath,
Expand Down
7 changes: 6 additions & 1 deletion electron/app/js/modelArchive.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
194 changes: 144 additions & 50 deletions electron/app/js/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,47 @@ 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';
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) {
Expand All @@ -45,24 +79,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) {
Expand All @@ -72,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) {
Expand Down Expand Up @@ -132,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);
});
Expand All @@ -149,7 +177,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];
}
Expand All @@ -161,7 +192,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];
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -310,26 +372,31 @@ 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);
writeFile(projectFileName, projectContentsJson, {encoding: 'utf8'})
.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');
Expand Down Expand Up @@ -385,6 +452,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));
Expand Down Expand Up @@ -517,16 +585,16 @@ 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,
message: title,
filters: [
{name: i18n.t(projectFileTypeKey), extensions: [projectFileExtension]}
],
buttonLabel: i18n.t('button-save'),
buttonLabel: i18n.t(buttonKey),
properties: [
'createDirectory',
'showOverwriteConfirmation'
Expand All @@ -536,6 +604,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);
}

Expand Down Expand Up @@ -751,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) {
Expand Down Expand Up @@ -862,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;
}
Expand Down
Loading