Skip to content

Commit 701c2d8

Browse files
Fixing issues when saving the project encounters errors (#131)
* adding upfront check to make sure the user has write permission on the selected directory for the project file * catching and handling writeFile errors when saving the project
1 parent 4be0468 commit 701c2d8

File tree

8 files changed

+201
-68
lines changed

8 files changed

+201
-68
lines changed

electron/app/js/fsUtils.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Copyright (c) 2021, 2022, Oracle and/or its affiliates.
44
* Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
55
*/
6+
const { constants } = require('fs');
67
const fsPromises = require('fs/promises');
78
const path = require('path');
89
const osUtils = require('./osUtils');
@@ -292,6 +293,22 @@ async function getDirectoryForPath(fileSystemPath) {
292293
});
293294
}
294295

296+
async function canWriteInDirectory(filePath) {
297+
return new Promise(resolve => {
298+
isDirectory(filePath).then(isDir => {
299+
let pathToCheck = filePath;
300+
if (!isDir) {
301+
pathToCheck = path.dirname(filePath);
302+
}
303+
fsPromises.access(pathToCheck, constants.R_OK | constants.W_OK).then(() => {
304+
resolve(true);
305+
}).catch(() => {
306+
resolve(false);
307+
});
308+
});
309+
});
310+
}
311+
295312
async function _getFilesRecursivelyFromDirectory(directory, fileList) {
296313
const i18n = require('./i18next.config');
297314

@@ -331,6 +348,7 @@ async function _processDirectoryListing(directory, listing, fileList) {
331348
}
332349

333350
module.exports = {
351+
canWriteInDirectory,
334352
createTemporaryDirectory,
335353
exists,
336354
getAbsolutePath,

electron/app/js/modelArchive.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,11 +355,16 @@ async function _addDirectoryToArchiveFile(archiveFile, zip, zipPath, dirPath) {
355355
}
356356

357357
async function _removePathFromArchive(archiveFile, zip, zipPath) {
358+
if (!zipPath) {
359+
getLogger().warn('_removePathFromArchive received empty zipPath so skipping...');
360+
return Promise.resolve();
361+
}
362+
358363
return new Promise((resolve, reject) => {
359364
try {
360365
if (zip.file(zipPath)) {
361366
zip.remove(zipPath);
362-
} else if (zipPath.endsWith('/')) {
367+
} else if (zipPath && zipPath.endsWith('/')) {
363368
// Remove the trailing slash so the target folder is also removed, not just its contents...
364369
zip.remove(zipPath.slice(0, -1));
365370
}

electron/app/js/project.js

Lines changed: 144 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,47 @@ const { getLogger } = require('./wktLogging');
1717
const { sendToWindow } = require('./windowUtils');
1818
const i18n = require('./i18next.config');
1919
const { CredentialStoreManager, EncryptedCredentialManager, CredentialNoStoreManager } = require('./credentialManager');
20+
const errorUtils = require('./errorUtils');
2021

2122
const projectFileTypeKey = 'dialog-wktFileType';
2223
const projectFileExtension = 'wktproj';
2324
const emptyProjectContents = {};
2425

2526
const openProjects = new Map();
2627

28+
// This file is the central file controlling all the project create and save functionality.
29+
// As such, there are a number of flows that make 1 or more calls into the methods in this file.
30+
//
31+
// Create New Project menu item flow:
32+
// - The entry point is createNewProject():
33+
// + asks the user for the project file location
34+
// + sends the start-new-project message to the renderer
35+
// - On receiving the start-new-project message:
36+
// + the renderer gathers any project-related state
37+
// + sends the new-project message
38+
// - The new-project message calls initializeNewProject():
39+
// + writes the file
40+
// + handles other necessary state management if the file write succeeds
41+
//
42+
// Save All menu item flow:
43+
// - The entry point is startSaveProject():
44+
// + sends the start-save-project message to renderer
45+
// - On receiving the start-save-project message:
46+
// + determines if the project needs saving
47+
// + invokes confirm-project-file
48+
// - The confirm-project-file calls confirmProjectFile():
49+
// + gets the project file name, prompting the user if required
50+
// + returns the name, uuid, and file name (or null if file is not selected)
51+
// - On receiving the response to the confirm-project-file invocation, the renderer:
52+
// + gathers the project-related data
53+
// + invokes save-project
54+
// - The save-project calls saveProject()
55+
// + saves the project file
56+
// + if project file save succeeds, saves any model files
57+
// + returns whether the save was successful and the model file contents
58+
// - On receiving the response to the save-project invocation, the renderer ends the flow
59+
//
60+
2761
// Public methods
2862
//
2963
function isWktProjectFile(filename) {
@@ -45,24 +79,18 @@ function showExistingProjectWindow(existingProjectWindow) {
4579
}
4680

4781
async function createNewProject(targetWindow) {
48-
const saveResponse = await dialog.showSaveDialog(targetWindow, {
49-
title: 'Create WebLogic Kubernetes Toolkit Project',
50-
buttonLabel: 'Create Project',
51-
filters: [
52-
{ name: i18n.t(projectFileTypeKey), extensions: [projectFileExtension] }
53-
],
54-
properties: [
55-
'createDirectory',
56-
'showOverwriteConfirmation'
57-
]
82+
const titleKey = 'dialog-createNewProjectTitle';
83+
const buttonKey = 'button-create';
84+
85+
return new Promise(resolve => {
86+
_chooseProjectSaveFile(targetWindow,titleKey, buttonKey).then(projectFileName => {
87+
if (projectFileName) {
88+
sendToWindow(targetWindow, 'start-new-project', projectFileName);
89+
// window will reply with new-project -> initializeNewProject() including isDirty flag
90+
}
91+
resolve();
92+
});
5893
});
59-
60-
if (saveResponse.canceled || !saveResponse.filePath || projectFileAlreadyOpen(saveResponse.filePath)) {
61-
return;
62-
}
63-
64-
sendToWindow(targetWindow, 'start-new-project', saveResponse.filePath);
65-
// window will reply with new-project -> initializeNewProject() including isDirty flag
6694
}
6795

6896
async function initializeNewProject(targetWindow, projectFile, isDirty) {
@@ -72,21 +100,21 @@ async function initializeNewProject(targetWindow, projectFile, isDirty) {
72100
return;
73101
}
74102

75-
let projectFileName = getProjectFileName(projectFile);
76-
if (path.extname(projectFileName) !== `.${projectFileExtension}`) {
77-
projectFileName = `${projectFileName}.${projectFileExtension}`;
78-
}
103+
const projectFileName = getProjectFileName(projectFile);
79104
const wktWindow = require('./wktWindow');
80-
if(projectWindow.id === targetWindow.id) {
81-
wktWindow.setTitleFileName(projectWindow, projectFileName, false);
82-
} else {
83-
projectWindow.on('ready-to-show', () => {
105+
106+
const wroteFile = await _createNewProjectFile(projectWindow, projectFileName);
107+
if (wroteFile) {
108+
if (projectWindow.id === targetWindow.id) {
84109
wktWindow.setTitleFileName(projectWindow, projectFileName, false);
85-
});
110+
} else {
111+
projectWindow.on('ready-to-show', () => {
112+
wktWindow.setTitleFileName(projectWindow, projectFileName, false);
113+
});
114+
}
115+
app.addRecentDocument(projectFileName);
116+
projectWindow.setRepresentedFilename(projectFileName);
86117
}
87-
await _createNewProjectFile(projectWindow, projectFileName);
88-
app.addRecentDocument(projectFileName);
89-
projectWindow.setRepresentedFilename(projectFileName);
90118
}
91119

92120
async function openProject(targetWindow) {
@@ -132,7 +160,7 @@ async function openProjectFile(targetWindow, projectFile, isDirty) {
132160
.catch(err => {
133161
dialog.showErrorBox(
134162
i18n.t('dialog-openProjectFileErrorTitle'),
135-
i18n.t('dialog-openProjectFileErrorMessage', { projectFileName: projectFile, err: err }),
163+
i18n.t('dialog-openProjectFileErrorMessage', { projectFileName: projectFile, err: errorUtils.getErrorMessage(err) }),
136164
);
137165
getLogger().error('Failed to open project file %s: %s', projectFile, err);
138166
});
@@ -149,7 +177,10 @@ async function confirmProjectFile(targetWindow) {
149177
projectFile = await _chooseProjectSaveFile(targetWindow);
150178
projectName = projectFile ? _generateProjectName(projectFile) : null;
151179
projectUuid = projectFile ? _generateProjectUuid() : null;
152-
app.addRecentDocument(projectFile);
180+
if (projectFile) {
181+
getLogger().debug('confirmProjectFile adding %s to recent documents', projectFile);
182+
app.addRecentDocument(projectFile);
183+
}
153184
}
154185
return [projectFile, projectName, projectUuid];
155186
}
@@ -161,7 +192,10 @@ async function chooseProjectFile(targetWindow) {
161192
const projectFile = await _chooseProjectSaveFile(targetWindow);
162193
const projectName = projectFile ? _generateProjectName(projectFile) : null;
163194
const projectUuid = projectFile ? _generateProjectUuid() : null;
164-
app.addRecentDocument(projectFile);
195+
if (projectFile) {
196+
getLogger().debug('chooseProjectFile adding %s to recent documents', projectFile);
197+
app.addRecentDocument(projectFile);
198+
}
165199
return [projectFile, projectName, projectUuid];
166200
}
167201

@@ -179,15 +213,43 @@ function startSaveProjectAs(targetWindow) {
179213

180214
// save the specified project and model contents to the project file.
181215
// usually invoked by the save-project IPC invocation.
182-
async function saveProject(targetWindow, projectFile, projectContents, externalFileContents) {
216+
async function saveProject(targetWindow, projectFile, projectContents, externalFileContents, showErrors = true) {
183217
// the result will contain only sections that were updated due to save, such as model.archiveFiles
184-
const saveResult = {};
218+
const saveResult = {
219+
isProjectFileSaved: false,
220+
areModelFilesSaved: false
221+
};
185222

186-
_assignProjectFile(targetWindow, projectFile);
187-
saveResult['model'] = await _saveExternalFileContents(_getProjectDirectory(targetWindow), externalFileContents);
188-
await _saveProjectFile(targetWindow, projectFile, projectContents);
189-
const wktWindow = require('./wktWindow');
190-
wktWindow.setTitleFileName(targetWindow, projectFile, false);
223+
const assignProjectFileData = _assignProjectFile(targetWindow, projectFile);
224+
try {
225+
await _saveProjectFile(targetWindow, projectFile, projectContents);
226+
saveResult.isProjectFileSaved = true;
227+
} catch (err) {
228+
if (showErrors) {
229+
_showSaveError(projectFile, err);
230+
}
231+
getLogger().error('Failed to save project file %s: %s', projectFile, err);
232+
// revert the project assignment to the window
233+
_revertAssignProjectFile(assignProjectFileData);
234+
saveResult.reason = i18n.t('dialog-saveProjectFileErrorMessage', { projectFileName: projectFile, err: err });
235+
}
236+
237+
if (saveResult.isProjectFileSaved) {
238+
const wktWindow = require('./wktWindow');
239+
wktWindow.setTitleFileName(targetWindow, projectFile, false);
240+
try {
241+
saveResult['model'] = await _saveExternalFileContents(_getProjectDirectory(targetWindow), externalFileContents);
242+
saveResult.areModelFilesSaved = true;
243+
} catch (err) {
244+
const message = i18n.t('project-save-model-files-error-message', { error: errorUtils.getErrorMessage(err) });
245+
if (showErrors) {
246+
const title = i18n.t('project-save-model-files-error-title');
247+
dialog.showErrorBox(title, message);
248+
}
249+
getLogger().error('Failed to save one of the model files for project file %s: %s', projectFile, err);
250+
saveResult.reason = message;
251+
}
252+
}
191253
return saveResult;
192254
}
193255

@@ -310,26 +372,31 @@ async function exportArchiveFile(targetWindow, archivePath, projectFile) {
310372
// Private helper methods
311373
//
312374
async function _createNewProjectFile(targetWindow, projectFileName) {
375+
getLogger().debug('entering _createNewProjectFile() for %s', projectFileName);
313376
return new Promise((resolve) => {
314377
const projectContents = _addProjectIdentifiers(projectFileName, emptyProjectContents);
315378
const projectContentsJson = JSON.stringify(projectContents, null, 2);
316379
writeFile(projectFileName, projectContentsJson, {encoding: 'utf8'})
317380
.then(() => {
318381
_addOpenProject(targetWindow, projectFileName, false, new CredentialStoreManager(projectContents.uuid));
319382
sendToWindow(targetWindow, 'project-created', projectFileName, projectContents);
320-
resolve();
383+
resolve(true);
321384
})
322385
.catch(err => {
323-
dialog.showErrorBox(
324-
i18n.t('dialog-saveProjectFileErrorTitle'),
325-
i18n.t('dialog-saveProjectFileErrorMessage', { projectFileName: projectFileName, err: err }),
326-
);
386+
_showSaveError(projectFileName, err);
327387
getLogger().error('Failed to save new project in file %s: %s', projectFileName, err);
328-
resolve();
388+
resolve(false);
329389
});
330390
});
331391
}
332392

393+
function _showSaveError(projectFileName, err) {
394+
dialog.showErrorBox(
395+
i18n.t('dialog-saveProjectFileErrorTitle'),
396+
i18n.t('dialog-saveProjectFileErrorMessage', { projectFileName: projectFileName, err: err }),
397+
);
398+
}
399+
333400
function _addProjectIdentifiers(projectFileName, projectContents) {
334401
const alreadyHasName = Object.prototype.hasOwnProperty.call(projectContents, 'name');
335402
const alreadyHasGuid = Object.prototype.hasOwnProperty.call(projectContents, 'uuid');
@@ -385,6 +452,7 @@ async function _openProjectFile(targetWindow, projectFileName) {
385452
const wktWindow = require('./wktWindow');
386453
wktWindow.setTitleFileName(targetWindow, projectFileName, false);
387454
targetWindow.setRepresentedFilename(projectFileName);
455+
getLogger().debug('_openProjectFile adding %s to recent documents', projectFileName);
388456
app.addRecentDocument(projectFileName);
389457
resolve();
390458
}).catch(err => reject(err));
@@ -517,16 +585,16 @@ async function _sendProjectOpened(targetWindow, file, jsonContents) {
517585
sendToWindow(targetWindow, 'project-opened', file, jsonContents, modelFilesContentJson);
518586
}
519587

520-
async function _chooseProjectSaveFile(targetWindow) {
521-
const title = i18n.t('dialog-chooseProjectSaveFile');
588+
async function _chooseProjectSaveFile(targetWindow, titleKey = 'dialog-chooseProjectSaveFile', buttonKey = 'button-save') {
589+
const title = i18n.t(titleKey);
522590

523591
let saveResponse = await dialog.showSaveDialog(targetWindow, {
524592
title: title,
525593
message: title,
526594
filters: [
527595
{name: i18n.t(projectFileTypeKey), extensions: [projectFileExtension]}
528596
],
529-
buttonLabel: i18n.t('button-save'),
597+
buttonLabel: i18n.t(buttonKey),
530598
properties: [
531599
'createDirectory',
532600
'showOverwriteConfirmation'
@@ -536,6 +604,17 @@ async function _chooseProjectSaveFile(targetWindow) {
536604
if (saveResponse.canceled || !saveResponse.filePath || projectFileAlreadyOpen(saveResponse.filePath)) {
537605
return null;
538606
}
607+
608+
// Do a quick sanity check to make sure that the user has permissions to
609+
// write to the directory chosen. If not, show them the error and return null.
610+
//
611+
if (! await fsUtils.canWriteInDirectory(saveResponse.filePath)) {
612+
const errTitle = i18n.t('dialog-projectSaveFileLocationNotWritableTitle');
613+
const errMessage = i18n.t('dialog-projectSaveFileLocationNotWritableError',
614+
{ projectFileDirectory: path.dirname(saveResponse.filePath)});
615+
dialog.showErrorBox(errTitle, errMessage);
616+
return null;
617+
}
539618
return getProjectFileName(saveResponse.filePath);
540619
}
541620

@@ -751,6 +830,21 @@ function _assignProjectFile(targetWindow, projectFile) {
751830
if (newFile !== oldFile) {
752831
_addOpenProject(targetWindow, projectFile, false);
753832
}
833+
return {
834+
existingProject,
835+
oldFile,
836+
newFile
837+
};
838+
}
839+
840+
function _revertAssignProjectFile(targetWindow, assignProjectFileData) {
841+
if (assignProjectFileData.existingProject) {
842+
if (assignProjectFileData.oldFile !== assignProjectFileData.newFile) {
843+
_addOpenProject(targetWindow, assignProjectFileData.oldFile, false);
844+
}
845+
} else {
846+
openProjects.delete(targetWindow);
847+
}
754848
}
755849

756850
async function _createCredentialManager(targetWindow, projectFileJsonContent) {
@@ -862,8 +956,8 @@ function _setCredentialManager(targetWindow, credentialManager) {
862956
// On Linux, the save dialog does not automatically add the project file extension...
863957
function getProjectFileName(dialogReturnedFileName) {
864958
let result = dialogReturnedFileName;
865-
if (dialogReturnedFileName && path.extname(dialogReturnedFileName) !== '.wktproj') {
866-
result = `${dialogReturnedFileName}.wktproj`;
959+
if (dialogReturnedFileName && path.extname(dialogReturnedFileName) !== `.${projectFileExtension}`) {
960+
result = `${dialogReturnedFileName}.${projectFileExtension}`;
867961
}
868962
return result;
869963
}

0 commit comments

Comments
 (0)