Skip to content

Commit f9b1a69

Browse files
author
Akos Kitta
committed
fix: sketch Save As errors on name collision
The Save As operation is halted and the problem clearly communicated to the user when there is a collision between the target primary source file name and existing secondary source files of the sketch. Closes #827 Signed-off-by: Akos Kitta <[email protected]>
1 parent 193583a commit f9b1a69

File tree

5 files changed

+107
-4
lines changed

5 files changed

+107
-4
lines changed

Diff for: arduino-ide-extension/src/browser/contributions/save-as-sketch.ts

+29-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { NavigatableWidget } from '@theia/core/lib/browser/navigatable';
33
import { Saveable } from '@theia/core/lib/browser/saveable';
44
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
55
import { WindowService } from '@theia/core/lib/browser/window/window-service';
6+
import { ApplicationError } from '@theia/core/lib/common/application-error';
67
import { nls } from '@theia/core/lib/common/nls';
78
import { inject, injectable } from '@theia/core/shared/inversify';
89
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
910
import { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service';
11+
import { SketchesError } from '../../common/protocol';
1012
import { StartupTasks } from '../../electron-common/startup-task';
1113
import { ArduinoMenus } from '../menu/arduino-menus';
1214
import { CurrentSketch } from '../sketches-service-client-impl';
@@ -35,7 +37,29 @@ export class SaveAsSketch extends CloudSketchContribution {
3537

3638
override registerCommands(registry: CommandRegistry): void {
3739
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
38-
execute: (args) => this.saveAs(args),
40+
execute: async (args) => {
41+
try {
42+
return await this.saveAs(args);
43+
} catch (err) {
44+
let message = String(err);
45+
if (ApplicationError.is(err)) {
46+
if (SketchesError.SketchAlreadyContainsThisFile.is(err)) {
47+
message = nls.localize(
48+
'arduino/sketch/sketchAlreadyContainsThisFileMessage',
49+
'Failed to save sketch "{0}" as "{1}". {2}',
50+
err.data.sourceSketchName,
51+
err.data.targetSketchName,
52+
err.message
53+
);
54+
} else {
55+
message = err.message;
56+
}
57+
} else if (err instanceof Error) {
58+
message = err.message;
59+
}
60+
this.messageService.error(message);
61+
}
62+
},
3963
});
4064
}
4165

@@ -58,13 +82,14 @@ export class SaveAsSketch extends CloudSketchContribution {
5882
* Resolves `true` if the sketch was successfully saved as something.
5983
*/
6084
private async saveAs(
61-
{
85+
params = SaveAsSketch.Options.DEFAULT
86+
): Promise<boolean> {
87+
const {
6288
execOnlyIfTemp,
6389
openAfterMove,
6490
wipeOriginal,
6591
markAsRecentlyOpened,
66-
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
67-
): Promise<boolean> {
92+
} = params;
6893
assertConnectedToBackend({
6994
connectionStatusService: this.connectionStatusService,
7095
messageService: this.messageService,

Diff for: arduino-ide-extension/src/common/protocol/sketches-service.ts

+16
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export namespace SketchesError {
99
NotFound: 5001,
1010
InvalidName: 5002,
1111
InvalidFolderName: 5003,
12+
SketchAlreadyContainsThisFile: 5004,
1213
};
1314
export const NotFound = ApplicationError.declare(
1415
Codes.NotFound,
@@ -37,6 +38,21 @@ export namespace SketchesError {
3738
};
3839
}
3940
);
41+
// https://github.com/arduino/arduino-ide/issues/827
42+
export const SketchAlreadyContainsThisFile = ApplicationError.declare(
43+
Codes.SketchAlreadyContainsThisFile,
44+
(
45+
message: string,
46+
sourceSketchName: string,
47+
targetSketchName: string,
48+
existingSketchFilename: string
49+
) => {
50+
return {
51+
message,
52+
data: { sourceSketchName, targetSketchName, existingSketchFilename },
53+
};
54+
}
55+
);
4056
}
4157

4258
export const SketchesServicePath = '/services/sketches-service';

Diff for: arduino-ide-extension/src/node/sketches-service-impl.ts

+29
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,35 @@ export class SketchesServiceImpl
530530
throw SketchesError.InvalidFolderName(message, destinationFolderBasename);
531531
}
532532

533+
// verify a possible name collision with existing ino files
534+
if (sourceFolderBasename !== destinationFolderBasename) {
535+
try {
536+
const otherInoBasename = `${destinationFolderBasename}.ino`;
537+
const otherInoPath = join(source, otherInoBasename);
538+
const stat = await fs.stat(otherInoPath);
539+
if (stat.isFile()) {
540+
const message = nls.localize(
541+
'arduino/sketch/sketchAlreadyContainsThisFileError',
542+
"The sketch already contains a file named '{0}'",
543+
otherInoBasename
544+
);
545+
// if can read the file, it will be a collision
546+
throw SketchesError.SketchAlreadyContainsThisFile(
547+
message,
548+
sourceFolderBasename,
549+
destinationFolderBasename,
550+
otherInoBasename
551+
);
552+
}
553+
// Otherwise, it's OK, if it is an existing directory
554+
} catch (err) {
555+
// It's OK if the file is missing.
556+
if (!ErrnoException.isENOENT(err)) {
557+
throw err;
558+
}
559+
}
560+
}
561+
533562
let filter: CopyOptions['filter'];
534563
if (onlySketchFiles) {
535564
const sketchFilePaths = Sketch.uris(sketch).map(FileUri.fsPath);

Diff for: arduino-ide-extension/src/test/node/sketches-service-impl.slow-test.ts

+31
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,37 @@ describe('sketches-service-impl', () => {
158158
);
159159
});
160160

161+
it('should error when the destination sketch folder name collides with an existing sketch file name', async () => {
162+
const sketchesService =
163+
container.get<SketchesServiceImpl>(SketchesService);
164+
const tempDirPath = await sketchesService['createTempFolder']();
165+
const destinationPath = join(tempDirPath, 'ExistingSketchFile');
166+
let sketch = await sketchesService.createNewSketch();
167+
toDispose.push(disposeSketch(sketch));
168+
const sourcePath = FileUri.fsPath(sketch.uri);
169+
const otherInoBasename = 'ExistingSketchFile.ino';
170+
const otherInoPath = join(sourcePath, otherInoBasename);
171+
await fs.writeFile(otherInoPath, '// a comment', { encoding: 'utf8' });
172+
173+
sketch = await sketchesService.loadSketch(sketch.uri);
174+
expect(Sketch.isInSketch(FileUri.create(otherInoPath), sketch)).to.be
175+
.true;
176+
177+
await rejects(
178+
sketchesService.copy(sketch, {
179+
destinationUri: FileUri.create(destinationPath).toString(),
180+
}),
181+
(reason) => {
182+
return (
183+
typeof reason === 'object' &&
184+
reason !== null &&
185+
SketchesError.SketchAlreadyContainsThisFile.is(reason) &&
186+
reason.data.existingSketchFilename === otherInoBasename
187+
);
188+
}
189+
);
190+
});
191+
161192
it('should copy a sketch when the destination does not exist', async () => {
162193
const sketchesService =
163194
container.get<SketchesServiceImpl>(SketchesService);

Diff for: i18n/en.json

+2
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,8 @@
460460
"saveSketchAs": "Save sketch folder as...",
461461
"showFolder": "Show Sketch Folder",
462462
"sketch": "Sketch",
463+
"sketchAlreadyContainsThisFileError": "The sketch already contains a file named '{0}'",
464+
"sketchAlreadyContainsThisFileMessage": "Failed to save sketch \"{0}\" as \"{1}\". {2}",
463465
"sketchbook": "Sketchbook",
464466
"titleLocalSketchbook": "Local Sketchbook",
465467
"titleSketchbook": "Sketchbook",

0 commit comments

Comments
 (0)