From 27be2120c632cbd2c863b85f93663473c25ad5dc Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Mon, 13 Feb 2023 16:44:22 +0100 Subject: [PATCH] feat: copy sketch to the cloud Closes #1876 Signed-off-by: Akos Kitta --- .../browser/arduino-ide-frontend-module.ts | 2 + .../contributions/create-cloud-copy.ts | 117 ++++++++++++++++++ .../browser/contributions/new-cloud-sketch.ts | 59 +++++++-- .../contributions/rename-cloud-sketch.ts | 2 +- .../browser/contributions/save-as-sketch.ts | 102 +++++++-------- .../widgets/sketchbook/sketchbook-commands.ts | 3 - .../sketchbook/sketchbook-tree-widget.tsx | 25 ++-- .../widgets/sketchbook/sketchbook-tree.ts | 18 ++- .../src/common/protocol/sketches-service.ts | 13 +- .../src/node/sketches-service-impl.ts | 33 ++++- i18n/en.json | 1 + 11 files changed, 293 insertions(+), 82 deletions(-) create mode 100644 arduino-ide-extension/src/browser/contributions/create-cloud-copy.ts diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 774345ac6..ca2a2988e 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -352,6 +352,7 @@ import { CreateFeatures } from './create/create-features'; import { Account } from './contributions/account'; import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget'; import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget'; +import { CreateCloudCopy } from './contributions/create-cloud-copy'; export default new ContainerModule((bind, unbind, isBound, rebind) => { // Commands and toolbar items @@ -741,6 +742,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { Contribution.configure(bind, RenameCloudSketch); Contribution.configure(bind, Account); Contribution.configure(bind, CloudSketchbookContribution); + Contribution.configure(bind, CreateCloudCopy); bindContributionProvider(bind, StartupTaskProvider); bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window diff --git a/arduino-ide-extension/src/browser/contributions/create-cloud-copy.ts b/arduino-ide-extension/src/browser/contributions/create-cloud-copy.ts new file mode 100644 index 000000000..b45ea4ffe --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/create-cloud-copy.ts @@ -0,0 +1,117 @@ +import { FrontendApplication } from '@theia/core/lib/browser/frontend-application'; +import { ApplicationShell } from '@theia/core/lib/browser/shell'; +import type { Command, CommandRegistry } from '@theia/core/lib/common/command'; +import { Progress } from '@theia/core/lib/common/message-service-protocol'; +import { nls } from '@theia/core/lib/common/nls'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { Create } from '../create/typings'; +import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service'; +import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree'; +import { SketchbookTree } from '../widgets/sketchbook/sketchbook-tree'; +import { SketchbookTreeModel } from '../widgets/sketchbook/sketchbook-tree-model'; +import { CloudSketchContribution, pushingSketch } from './cloud-contribution'; +import { + CreateNewCloudSketchCallback, + NewCloudSketch, + NewCloudSketchParams, +} from './new-cloud-sketch'; +import { saveOntoCopiedSketch } from './save-as-sketch'; + +interface CreateCloudCopyParams { + readonly model: SketchbookTreeModel; + readonly node: SketchbookTree.SketchDirNode; +} +function isCreateCloudCopyParams(arg: unknown): arg is CreateCloudCopyParams { + return ( + (arg).model !== undefined && + (arg).model instanceof SketchbookTreeModel && + (arg).node !== undefined && + SketchbookTree.SketchDirNode.is((arg).node) + ); +} + +@injectable() +export class CreateCloudCopy extends CloudSketchContribution { + @inject(ApplicationConnectionStatusContribution) + private readonly connectionStatus: ApplicationConnectionStatusContribution; + + private shell: ApplicationShell; + + override onStart(app: FrontendApplication): void { + this.shell = app.shell; + } + + override registerCommands(registry: CommandRegistry): void { + registry.registerCommand(CreateCloudCopy.Commands.CREATE_CLOUD_COPY, { + execute: (args: CreateCloudCopyParams) => this.createCloudCopy(args), + isEnabled: (args: unknown) => + Boolean(this.createFeatures.session) && isCreateCloudCopyParams(args), + isVisible: (args: unknown) => + Boolean(this.createFeatures.enabled) && + Boolean(this.createFeatures.session) && + this.connectionStatus.offlineStatus !== 'internet' && + isCreateCloudCopyParams(args), + }); + } + + /** + * - creates new cloud sketch with the name of the params sketch, + * - pulls the cloud sketch, + * - copies files from params sketch to pulled cloud sketch in the cache folder, + * - pushes the cloud sketch, and + * - opens in new window. + */ + private async createCloudCopy(params: CreateCloudCopyParams): Promise { + const sketch = await this.sketchesService.loadSketch( + params.node.fileStat.resource.toString() + ); + const callback: CreateNewCloudSketchCallback = async ( + newSketch: Create.Sketch, + newNode: CloudSketchbookTree.CloudSketchDirNode, + progress: Progress + ) => { + const treeModel = await this.treeModel(); + if (!treeModel) { + throw new Error('Could not retrieve the cloud sketchbook tree model.'); + } + + progress.report({ + message: nls.localize( + 'arduino/createCloudCopy/copyingSketchFilesMessage', + 'Copying local sketch files...' + ), + }); + const localCacheFolderUri = newNode.uri.toString(); + await this.sketchesService.copy(sketch, { + destinationUri: localCacheFolderUri, + onlySketchFiles: true, + }); + await saveOntoCopiedSketch( + sketch, + localCacheFolderUri, + this.shell, + this.editorManager + ); + + progress.report({ message: pushingSketch(newSketch.name) }); + await treeModel.sketchbookTree().push(newNode, true); + }; + return this.commandService.executeCommand( + NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id, + { + initialValue: params.node.fileStat.name, + callback, + skipShowErrorMessageOnOpen: false, + } + ); + } +} + +export namespace CreateCloudCopy { + export namespace Commands { + export const CREATE_CLOUD_COPY: Command = { + id: 'arduino-create-cloud-copy', + iconClass: 'fa fa-arduino-cloud-upload', + }; + } +} diff --git a/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts b/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts index 4d449b121..e252e3f40 100644 --- a/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts @@ -6,7 +6,7 @@ import { Progress } from '@theia/core/lib/common/message-service-protocol'; import { nls } from '@theia/core/lib/common/nls'; import { injectable } from '@theia/core/shared/inversify'; import { CreateUri } from '../create/create-uri'; -import { isConflict } from '../create/typings'; +import { Create, isConflict } from '../create/typings'; import { ArduinoMenus } from '../menu/arduino-menus'; import { TaskFactoryImpl, @@ -15,13 +15,36 @@ import { import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree'; import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model'; import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands'; -import { Command, CommandRegistry, Sketch } from './contribution'; import { CloudSketchContribution, pullingSketch, sketchAlreadyExists, synchronizingSketchbook, } from './cloud-contribution'; +import { Command, CommandRegistry, Sketch } from './contribution'; + +export interface CreateNewCloudSketchCallback { + ( + newSketch: Create.Sketch, + newNode: CloudSketchbookTree.CloudSketchDirNode, + progress: Progress + ): Promise; +} + +export interface NewCloudSketchParams { + /** + * Value to populate the dialog `` when it opens. + */ + readonly initialValue?: string | undefined; + /** + * Additional callback to call when the new cloud sketch has been created. + */ + readonly callback?: CreateNewCloudSketchCallback; + /** + * If `true`, the validation error message will not be visible in the input dialog, but the `OK` button will be disabled. Defaults to `true`. + */ + readonly skipShowErrorMessageOnOpen?: boolean; +} @injectable() export class NewCloudSketch extends CloudSketchContribution { @@ -43,7 +66,14 @@ export class NewCloudSketch extends CloudSketchContribution { override registerCommands(registry: CommandRegistry): void { registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, { - execute: () => this.createNewSketch(true), + execute: (params: NewCloudSketchParams) => + this.createNewSketch( + typeof params?.skipShowErrorMessageOnOpen === 'boolean' + ? params.skipShowErrorMessageOnOpen + : true, + params?.initialValue, + params?.callback + ), isEnabled: () => Boolean(this.createFeatures.session), isVisible: () => this.createFeatures.enabled, }); @@ -66,7 +96,8 @@ export class NewCloudSketch extends CloudSketchContribution { private async createNewSketch( skipShowErrorMessageOnOpen: boolean, - initialValue?: string | undefined + initialValue?: string | undefined, + callback?: CreateNewCloudSketchCallback ): Promise { const treeModel = await this.treeModel(); if (treeModel) { @@ -75,7 +106,8 @@ export class NewCloudSketch extends CloudSketchContribution { rootNode, treeModel, skipShowErrorMessageOnOpen, - initialValue + initialValue, + callback ); } } @@ -84,13 +116,14 @@ export class NewCloudSketch extends CloudSketchContribution { rootNode: CompositeTreeNode, treeModel: CloudSketchbookTreeModel, skipShowErrorMessageOnOpen: boolean, - initialValue?: string | undefined + initialValue?: string | undefined, + callback?: CreateNewCloudSketchCallback ): Promise { const existingNames = rootNode.children .filter(CloudSketchbookTree.CloudSketchDirNode.is) .map(({ fileStat }) => fileStat.name); const taskFactory = new TaskFactoryImpl((value) => - this.createNewSketchWithProgress(treeModel, value) + this.createNewSketchWithProgress(treeModel, value, callback) ); try { const dialog = new WorkspaceInputDialogWithProgress( @@ -118,7 +151,11 @@ export class NewCloudSketch extends CloudSketchContribution { } catch (err) { if (isConflict(err)) { await treeModel.refresh(); - return this.createNewSketch(false, taskFactory.value ?? initialValue); + return this.createNewSketch( + false, + taskFactory.value ?? initialValue, + callback + ); } throw err; } @@ -126,7 +163,8 @@ export class NewCloudSketch extends CloudSketchContribution { private createNewSketchWithProgress( treeModel: CloudSketchbookTreeModel, - value: string + value: string, + callback?: CreateNewCloudSketchCallback ): ( progress: Progress ) => Promise { @@ -143,6 +181,9 @@ export class NewCloudSketch extends CloudSketchContribution { await treeModel.refresh(); progress.report({ message: pullingSketch(sketch.name) }); const node = await this.pull(sketch); + if (callback && node) { + await callback(sketch, node, progress); + } return node; }; } diff --git a/arduino-ide-extension/src/browser/contributions/rename-cloud-sketch.ts b/arduino-ide-extension/src/browser/contributions/rename-cloud-sketch.ts index faed8d070..5cee63f0e 100644 --- a/arduino-ide-extension/src/browser/contributions/rename-cloud-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/rename-cloud-sketch.ts @@ -123,7 +123,7 @@ export class RenameCloudSketch extends CloudSketchContribution { const toPosixPath = params.cloudUri.parent.resolve(value).path.toString(); // push progress.report({ message: pushingSketch(params.sketch.name) }); - await treeModel.sketchbookTree().push(node); + await treeModel.sketchbookTree().push(node, true); // rename progress.report({ diff --git a/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts b/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts index 82fcec530..1ef955f45 100644 --- a/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts @@ -6,6 +6,7 @@ import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shel import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { nls } from '@theia/core/lib/common/nls'; import { inject, injectable } from '@theia/core/shared/inversify'; +import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; import { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service'; import { StartupTask } from '../../electron-common/startup-task'; import { ArduinoMenus } from '../menu/arduino-menus'; @@ -28,7 +29,7 @@ import { @injectable() export class SaveAsSketch extends CloudSketchContribution { @inject(ApplicationShell) - private readonly applicationShell: ApplicationShell; + private readonly shell: ApplicationShell; @inject(WindowService) private readonly windowService: WindowService; @@ -87,7 +88,12 @@ export class SaveAsSketch extends CloudSketchContribution { return false; } - await this.saveOntoCopiedSketch(sketch, newWorkspaceUri); + await saveOntoCopiedSketch( + sketch, + newWorkspaceUri, + this.shell, + this.editorManager + ); if (markAsRecentlyOpened) { this.sketchesService.markAsRecentlyOpened(newWorkspaceUri); } @@ -238,53 +244,6 @@ ${dialogContent.question}`.trim(); } return sketchFolderDestinationUri; } - - private async saveOntoCopiedSketch( - sketch: Sketch, - newSketchFolderUri: string - ): Promise { - const widgets = this.applicationShell.widgets; - const snapshots = new Map(); - for (const widget of widgets) { - const saveable = Saveable.getDirty(widget); - const uri = NavigatableWidget.getUri(widget); - if (!uri) { - continue; - } - const uriString = uri.toString(); - let relativePath: string; - if ( - uriString.includes(sketch.uri) && - saveable && - saveable.createSnapshot - ) { - // The main file will change its name during the copy process - // We need to store the new name in the map - if (sketch.mainFileUri === uriString) { - const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext; - relativePath = '/' + lastPart; - } else { - relativePath = uri.toString().substring(sketch.uri.length); - } - snapshots.set(relativePath, saveable.createSnapshot()); - } - } - await Promise.all( - Array.from(snapshots.entries()).map(async ([path, snapshot]) => { - const widgetUri = new URI(newSketchFolderUri + path); - try { - const widget = await this.editorManager.getOrCreateByUri(widgetUri); - const saveable = Saveable.get(widget); - if (saveable && saveable.applySnapshot) { - saveable.applySnapshot(snapshot); - await saveable.save(); - } - } catch (e) { - console.error(e); - } - }) - ); - } } interface InvalidSketchFolderDialogContent { @@ -317,3 +276,48 @@ export namespace SaveAsSketch { }; } } + +export async function saveOntoCopiedSketch( + sketch: Sketch, + newSketchFolderUri: string, + shell: ApplicationShell, + editorManager: EditorManager +): Promise { + const widgets = shell.widgets; + const snapshots = new Map(); + for (const widget of widgets) { + const saveable = Saveable.getDirty(widget); + const uri = NavigatableWidget.getUri(widget); + if (!uri) { + continue; + } + const uriString = uri.toString(); + let relativePath: string; + if (uriString.includes(sketch.uri) && saveable && saveable.createSnapshot) { + // The main file will change its name during the copy process + // We need to store the new name in the map + if (sketch.mainFileUri === uriString) { + const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext; + relativePath = '/' + lastPart; + } else { + relativePath = uri.toString().substring(sketch.uri.length); + } + snapshots.set(relativePath, saveable.createSnapshot()); + } + } + await Promise.all( + Array.from(snapshots.entries()).map(async ([path, snapshot]) => { + const widgetUri = new URI(newSketchFolderUri + path); + try { + const widget = await editorManager.getOrCreateByUri(widgetUri); + const saveable = Saveable.get(widget); + if (saveable && saveable.applySnapshot) { + saveable.applySnapshot(snapshot); + await saveable.save(); + } + } catch (e) { + console.error(e); + } + }) + ); +} diff --git a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-commands.ts b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-commands.ts index 50b5f9008..c8935d663 100644 --- a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-commands.ts +++ b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-commands.ts @@ -27,17 +27,14 @@ export namespace SketchbookCommands { export const OPEN_SKETCHBOOK_CONTEXT_MENU: Command = { id: 'arduino-sketchbook--open-sketch-context-menu', - label: 'Contextual menu', iconClass: 'sketchbook-tree__opts', }; export const SKETCHBOOK_HIDE_FILES: Command = { id: 'arduino-sketchbook--hide-files', - label: 'Contextual menu', }; export const SKETCHBOOK_SHOW_FILES: Command = { id: 'arduino-sketchbook--show-files', - label: 'Contextual menu', }; } diff --git a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-widget.tsx b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-widget.tsx index 20cab5d68..06ce63048 100644 --- a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-widget.tsx @@ -133,26 +133,37 @@ export class SketchbookTreeWidget extends FileTreeWidget { protected renderInlineCommands(node: TreeNode): React.ReactNode { if (SketchbookTree.SketchDirNode.is(node) && node.commands) { return Array.from(new Set(node.commands)).map((command) => - this.renderInlineCommand(command.id, node) + this.renderInlineCommand( + Array.isArray(command) + ? command + : typeof command === 'string' + ? command + : command.id, + node + ) ); } return undefined; } protected renderInlineCommand( - commandId: string, + command: string | [command: string, label: string], node: SketchbookTree.SketchDirNode, options?: any ): React.ReactNode { - const command = this.commandRegistry.getCommand(commandId); - const icon = command?.iconClass; + const commandId = Array.isArray(command) ? command[0] : command; + const resolvedCommand = this.commandRegistry.getCommand(commandId); + const icon = resolvedCommand?.iconClass; const args = { model: this.model, node: node, ...options }; if ( - command && + resolvedCommand && icon && this.commandRegistry.isEnabled(commandId, args) && this.commandRegistry.isVisible(commandId, args) ) { + const label = Array.isArray(command) + ? command[1] + : resolvedCommand.label ?? resolvedCommand.id; const className = [ TREE_NODE_SEGMENT_CLASS, TREE_NODE_TAIL_CLASS, @@ -162,9 +173,9 @@ export class SketchbookTreeWidget extends FileTreeWidget { ].join(' '); return (
{ event.preventDefault(); event.stopPropagation(); diff --git a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree.ts b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree.ts index 6726f12b6..86391c7db 100644 --- a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree.ts +++ b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree.ts @@ -9,6 +9,7 @@ import { WorkspaceRootNode, } from '@theia/navigator/lib/browser/navigator-tree'; import { ArduinoPreferences } from '../../arduino-preferences'; +import { nls } from '@theia/core/lib/common/nls'; @injectable() export class SketchbookTree extends FileNavigatorTree { @@ -18,7 +19,9 @@ export class SketchbookTree extends FileNavigatorTree { @inject(ArduinoPreferences) protected readonly arduinoPreferences: ArduinoPreferences; - override async resolveChildren(parent: CompositeTreeNode): Promise { + override async resolveChildren( + parent: CompositeTreeNode + ): Promise { const showAllFiles = this.arduinoPreferences['arduino.sketchbook.showAllFiles']; @@ -71,7 +74,13 @@ export class SketchbookTree extends FileNavigatorTree { protected async augmentSketchNode(node: DirNode): Promise { Object.assign(node, { type: 'sketch', - commands: [SketchbookCommands.OPEN_SKETCHBOOK_CONTEXT_MENU], + commands: [ + [ + 'arduino-create-cloud-copy', + nls.localize('arduino/createCloudCopy', 'Push Sketch to Cloud'), + ], + SketchbookCommands.OPEN_SKETCHBOOK_CONTEXT_MENU, + ], }); } @@ -96,7 +105,10 @@ export class SketchbookTree extends FileNavigatorTree { export namespace SketchbookTree { export interface SketchDirNode extends DirNode { readonly type: 'sketch'; - readonly commands?: Command[]; + /** + * Theia command, the command ID string, or a tuple of command ID and preferred UI label. If the array construct is used, the label is the 1st of the array. + */ + readonly commands?: (Command | string | [string, string])[]; } export namespace SketchDirNode { export function is( diff --git a/arduino-ide-extension/src/common/protocol/sketches-service.ts b/arduino-ide-extension/src/common/protocol/sketches-service.ts index df25a34f3..d2485220a 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service.ts @@ -74,12 +74,15 @@ export interface SketchesService { isTemp(sketch: SketchRef): Promise; /** - * If `isTemp` is `true` for the `sketch`, you can call this method to move the sketch from the temp - * location to `directories.user`. Resolves with the URI of the sketch after the move. Rejects, when the sketch - * was not in the temp folder. This method always overrides. It's the callers responsibility to ask the user whether - * the files at the destination can be overwritten or not. + * Recursively copies the sketch folder content including all files into the destination folder. + * Resolves with the new URI of the sketch after the move. This method always overrides. It's the callers responsibility to ask the user whether + * the files at the destination can be overwritten or not. This method copies all filesystem files, if you want to copy only sketch files, + * but exclude, for example, language server log file, set the `onlySketchFiles` property to `true`. `onlySketchFiles` is `false` by default. */ - copy(sketch: Sketch, options: { destinationUri: string }): Promise; + copy( + sketch: Sketch, + options: { destinationUri: string; onlySketchFiles?: boolean } + ): Promise; /** * Returns with the container sketch for the input `uri`. If the `uri` is not in a sketch folder, the promise resolves to `undefined`. diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index 42ca3e9a3..bf8b7aa47 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import * as glob from 'glob'; import * as crypto from 'crypto'; import * as PQueue from 'p-queue'; -import { ncp } from 'ncp'; +import { ncp, Options as NcpOptions } from 'ncp'; import { Mutable } from '@theia/core/lib/common/types'; import URI from '@theia/core/lib/common/uri'; import { ILogger } from '@theia/core/lib/common/logger'; @@ -495,7 +495,10 @@ export class SketchesServiceImpl async copy( sketch: Sketch, - { destinationUri }: { destinationUri: string } + { + destinationUri, + onlySketchFiles, + }: { destinationUri: string; onlySketchFiles?: boolean } ): Promise { const source = FileUri.fsPath(sketch.uri); const sketchExists = await exists(source); @@ -508,9 +511,29 @@ export class SketchesServiceImpl return sketch.uri; } - const copy = async (sourcePath: string, destinationPath: string) => { + const sketchFileUris = Sketch.uris(sketch); + const isInSketch = (filePath: string) => { + // filePath is either one of the sketch files or the container directory of one of the sketch files + const fileUri = FileUri.create(filePath).toString(); + return ( + sketchFileUris.includes(fileUri) || + sketchFileUris + .map((uri) => new URI(uri).parent.toString()) + .includes(fileUri) + ); + }; + + const filter: NcpOptions['filter'] = onlySketchFiles + ? (filePath: string) => isInSketch(filePath) + : undefined; + + const copy = async ( + sourcePath: string, + destinationPath: string, + options: NcpOptions = {} + ) => { return new Promise((resolve, reject) => { - ncp.ncp(sourcePath, destinationPath, async (error) => { + ncp.ncp(sourcePath, destinationPath, options, async (error) => { if (error) { reject(error); return; @@ -544,7 +567,7 @@ export class SketchesServiceImpl let tempDestination = await this.createTempFolder(); tempDestination = path.join(tempDestination, sketch.name); await fs.mkdir(tempDestination, { recursive: true }); - await copy(source, tempDestination); + await copy(source, tempDestination, { filter }); await copy(tempDestination, destination); return FileUri.create(destination).toString(); } diff --git a/i18n/en.json b/i18n/en.json index 7e6aea82c..25185dce0 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -196,6 +196,7 @@ "copyError": "Copy error messages", "noBoardSelected": "No board selected. Please select your Arduino board from the Tools > Board menu." }, + "createCloudCopy": "Push Sketch to Cloud", "daemon": { "restart": "Restart Daemon", "start": "Start Daemon",