mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-14 06:46:36 +00:00
[ATL-1454] Refactor pull/push to edit files in place (#464)
* improve push/pull process * improved diff tree performance generation * skip some files to be synced Co-authored-by: Francesco Stasi <f.stasi@me.com>
This commit is contained in:
parent
57b9eb95bb
commit
65152731f9
@ -93,7 +93,11 @@ export class CreateApi {
|
|||||||
|
|
||||||
async readDirectory(
|
async readDirectory(
|
||||||
posixPath: string,
|
posixPath: string,
|
||||||
options: { recursive?: boolean; match?: string } = {}
|
options: {
|
||||||
|
recursive?: boolean;
|
||||||
|
match?: string;
|
||||||
|
skipSketchCache?: boolean;
|
||||||
|
} = {}
|
||||||
): Promise<Create.Resource[]> {
|
): Promise<Create.Resource[]> {
|
||||||
const url = new URL(
|
const url = new URL(
|
||||||
`${this.domain()}/files/d/$HOME/sketches_v2${posixPath}`
|
`${this.domain()}/files/d/$HOME/sketches_v2${posixPath}`
|
||||||
@ -106,21 +110,29 @@ export class CreateApi {
|
|||||||
}
|
}
|
||||||
const headers = await this.headers();
|
const headers = await this.headers();
|
||||||
|
|
||||||
return this.run<Create.RawResource[]>(url, {
|
const cachedSketch = this.sketchCache.getSketch(posixPath);
|
||||||
|
|
||||||
|
const sketchPromise = options.skipSketchCache
|
||||||
|
? (cachedSketch && this.sketch(cachedSketch.id)) || Promise.resolve(null)
|
||||||
|
: Promise.resolve(this.sketchCache.getSketch(posixPath));
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
sketchPromise,
|
||||||
|
this.run<Create.RawResource[]>(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers,
|
headers,
|
||||||
})
|
}),
|
||||||
.then(async (result) => {
|
])
|
||||||
// add arduino_secrets.h to the results, when reading a sketch main folder
|
.then(async ([sketch, result]) => {
|
||||||
if (posixPath.length && posixPath !== posix.sep) {
|
if (posixPath.length && posixPath !== posix.sep) {
|
||||||
const sketch = this.sketchCache.getSketch(posixPath);
|
|
||||||
|
|
||||||
if (sketch && sketch.secrets && sketch.secrets.length > 0) {
|
if (sketch && sketch.secrets && sketch.secrets.length > 0) {
|
||||||
result.push(this.getSketchSecretStat(sketch));
|
result.push(this.getSketchSecretStat(sketch));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result.filter(
|
||||||
|
(res) => !Create.do_not_sync_files.includes(res.name)
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
if (reason?.status === 404) return [] as Create.Resource[];
|
if (reason?.status === 404) return [] as Create.Resource[];
|
||||||
|
@ -30,8 +30,6 @@ import { SketchesService } from '../../common/protocol';
|
|||||||
import { ArduinoPreferences } from '../arduino-preferences';
|
import { ArduinoPreferences } from '../arduino-preferences';
|
||||||
import { Create } from './typings';
|
import { Create } from './typings';
|
||||||
|
|
||||||
export const REMOTE_ONLY_FILES = ['sketch.json'];
|
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class CreateFsProvider
|
export class CreateFsProvider
|
||||||
implements
|
implements
|
||||||
@ -109,14 +107,10 @@ export class CreateFsProvider
|
|||||||
const resources = await this.getCreateApi.readDirectory(
|
const resources = await this.getCreateApi.readDirectory(
|
||||||
uri.path.toString()
|
uri.path.toString()
|
||||||
);
|
);
|
||||||
return resources
|
return resources.map(({ name, type }) => [name, this.toFileType(type)]);
|
||||||
.filter((res) => !REMOTE_ONLY_FILES.includes(res.name))
|
|
||||||
.map(({ name, type }) => [name, this.toFileType(type)]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(uri: URI, opts: FileDeleteOptions): Promise<void> {
|
async delete(uri: URI, opts: FileDeleteOptions): Promise<void> {
|
||||||
return;
|
|
||||||
|
|
||||||
if (!opts.recursive) {
|
if (!opts.recursive) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Arduino Create file-system provider does not support non-recursive deletion.'
|
'Arduino Create file-system provider does not support non-recursive deletion.'
|
||||||
|
@ -21,7 +21,7 @@ export namespace Create {
|
|||||||
|
|
||||||
export type ResourceType = 'sketch' | 'folder' | 'file';
|
export type ResourceType = 'sketch' | 'folder' | 'file';
|
||||||
export const arduino_secrets_file = 'arduino_secrets.h';
|
export const arduino_secrets_file = 'arduino_secrets.h';
|
||||||
export const do_not_sync_files = ['.theia'];
|
export const do_not_sync_files = ['.theia', 'sketch.json'];
|
||||||
export interface Resource {
|
export interface Resource {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
/**
|
/**
|
||||||
|
@ -22,6 +22,14 @@ export class SketchCache {
|
|||||||
return this.fileStats[path] || null;
|
return this.fileStats[path] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
purgeByPath(path: string): void {
|
||||||
|
for (const itemPath in this.fileStats) {
|
||||||
|
if (itemPath.indexOf(path) === 0) {
|
||||||
|
delete this.fileStats[itemPath];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addSketch(sketch: Create.Sketch): void {
|
addSketch(sketch: Create.Sketch): void {
|
||||||
const { path } = sketch;
|
const { path } = sketch;
|
||||||
const posixPath = toPosixPath(path);
|
const posixPath = toPosixPath(path);
|
||||||
|
@ -17,7 +17,6 @@ import {
|
|||||||
PreferenceScope,
|
PreferenceScope,
|
||||||
} from '@theia/core/lib/browser/preferences/preference-service';
|
} from '@theia/core/lib/browser/preferences/preference-service';
|
||||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||||
import { REMOTE_ONLY_FILES } from './../../create/create-fs-provider';
|
|
||||||
import { CreateApi } from '../../create/create-api';
|
import { CreateApi } from '../../create/create-api';
|
||||||
import { CreateUri } from '../../create/create-uri';
|
import { CreateUri } from '../../create/create-uri';
|
||||||
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
|
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
|
||||||
@ -33,10 +32,17 @@ import { ArduinoPreferences } from '../../arduino-preferences';
|
|||||||
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
|
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
|
||||||
import { FileStat } from '@theia/filesystem/lib/common/files';
|
import { FileStat } from '@theia/filesystem/lib/common/files';
|
||||||
import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree';
|
import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree';
|
||||||
|
import { posix, splitSketchPath } from '../../create/create-paths';
|
||||||
|
import { Create } from '../../create/typings';
|
||||||
|
|
||||||
const MESSAGE_TIMEOUT = 5 * 1000;
|
const MESSAGE_TIMEOUT = 5 * 1000;
|
||||||
const deepmerge = require('deepmerge').default;
|
const deepmerge = require('deepmerge').default;
|
||||||
|
|
||||||
|
type FilesToWrite = { source: URI; dest: URI };
|
||||||
|
type FilesToSync = {
|
||||||
|
filesToWrite: FilesToWrite[];
|
||||||
|
filesToDelete: URI[];
|
||||||
|
};
|
||||||
@injectable()
|
@injectable()
|
||||||
export class CloudSketchbookTree extends SketchbookTree {
|
export class CloudSketchbookTree extends SketchbookTree {
|
||||||
@inject(FileService)
|
@inject(FileService)
|
||||||
@ -94,7 +100,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
|||||||
|
|
||||||
async pull(arg: any): Promise<void> {
|
async pull(arg: any): Promise<void> {
|
||||||
const {
|
const {
|
||||||
model,
|
// model,
|
||||||
node,
|
node,
|
||||||
}: {
|
}: {
|
||||||
model: CloudSketchbookTreeModel;
|
model: CloudSketchbookTreeModel;
|
||||||
@ -127,47 +133,12 @@ export class CloudSketchbookTree extends SketchbookTree {
|
|||||||
const commandsCopy = node.commands;
|
const commandsCopy = node.commands;
|
||||||
node.commands = [];
|
node.commands = [];
|
||||||
|
|
||||||
// check if the sketch dir already exist
|
const localUri = await this.fileService.toUnderlyingResource(
|
||||||
if (CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
|
LocalCacheUri.root.resolve(node.remoteUri.path)
|
||||||
const filesToPull = (
|
|
||||||
await this.createApi.readDirectory(node.remoteUri.path.toString())
|
|
||||||
).filter((file: any) => !REMOTE_ONLY_FILES.includes(file.name));
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
filesToPull.map((file: any) => {
|
|
||||||
const uri = CreateUri.toUri(file);
|
|
||||||
this.fileService.copy(uri, LocalCacheUri.root.resolve(uri.path), {
|
|
||||||
overwrite: true,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
await this.sync(node.remoteUri, localUri);
|
||||||
|
|
||||||
// open the pulled files in the current workspace
|
this.sketchCache.purgeByPath(node.remoteUri.path.toString());
|
||||||
const currentSketch = await this.sketchServiceClient.currentSketch();
|
|
||||||
|
|
||||||
if (
|
|
||||||
!CreateUri.is(node.uri) &&
|
|
||||||
currentSketch &&
|
|
||||||
currentSketch.uri === node.uri.toString()
|
|
||||||
) {
|
|
||||||
filesToPull.forEach(async (file) => {
|
|
||||||
const localUri = LocalCacheUri.root.resolve(
|
|
||||||
CreateUri.toUri(file).path
|
|
||||||
);
|
|
||||||
const underlying = await this.fileService.toUnderlyingResource(
|
|
||||||
localUri
|
|
||||||
);
|
|
||||||
|
|
||||||
model.open(underlying);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await this.fileService.copy(
|
|
||||||
node.remoteUri,
|
|
||||||
LocalCacheUri.root.resolve(node.uri.path),
|
|
||||||
{ overwrite: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
node.commands = commandsCopy;
|
node.commands = commandsCopy;
|
||||||
this.messageService.info(`Done pulling ‘${node.fileStat.name}’.`, {
|
this.messageService.info(`Done pulling ‘${node.fileStat.name}’.`, {
|
||||||
@ -214,17 +185,107 @@ export class CloudSketchbookTree extends SketchbookTree {
|
|||||||
}
|
}
|
||||||
const commandsCopy = node.commands;
|
const commandsCopy = node.commands;
|
||||||
node.commands = [];
|
node.commands = [];
|
||||||
// delete every first level file, then push everything
|
|
||||||
const result = await this.fileService.copy(node.uri, node.remoteUri, {
|
const localUri = await this.fileService.toUnderlyingResource(
|
||||||
overwrite: true,
|
LocalCacheUri.root.resolve(node.remoteUri.path)
|
||||||
});
|
);
|
||||||
|
await this.sync(localUri, node.remoteUri);
|
||||||
|
|
||||||
|
this.sketchCache.purgeByPath(node.remoteUri.path.toString());
|
||||||
|
|
||||||
node.commands = commandsCopy;
|
node.commands = commandsCopy;
|
||||||
this.messageService.info(`Done pushing ‘${result.name}’.`, {
|
this.messageService.info(`Done pushing ‘${node.fileStat.name}’.`, {
|
||||||
timeout: MESSAGE_TIMEOUT,
|
timeout: MESSAGE_TIMEOUT,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async recursiveURIs(uri: URI): Promise<URI[]> {
|
||||||
|
// remote resources can be fetched one-shot via api
|
||||||
|
if (CreateUri.is(uri)) {
|
||||||
|
const resources = await this.createApi.readDirectory(
|
||||||
|
uri.path.toString(),
|
||||||
|
{ recursive: true, skipSketchCache: true }
|
||||||
|
);
|
||||||
|
return resources.map((resource) =>
|
||||||
|
CreateUri.toUri(splitSketchPath(resource.path)[1])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileStat = await this.fileService.resolve(uri, {
|
||||||
|
resolveMetadata: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fileStat.children || !fileStat.isDirectory) {
|
||||||
|
return [fileStat.resource];
|
||||||
|
}
|
||||||
|
|
||||||
|
let childrenUris: URI[] = [];
|
||||||
|
|
||||||
|
for await (const child of fileStat.children) {
|
||||||
|
childrenUris = [
|
||||||
|
...childrenUris,
|
||||||
|
...(await this.recursiveURIs(child.resource)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [fileStat.resource, ...childrenUris];
|
||||||
|
}
|
||||||
|
|
||||||
|
private URIsToMap(uris: URI[], basepath: string): Record<string, URI> {
|
||||||
|
return uris.reduce((prev: Record<string, URI>, curr) => {
|
||||||
|
const path = curr.toString().split(basepath);
|
||||||
|
|
||||||
|
if (path.length !== 2 || path[1].length === 0) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do not map "do_not_sync" files/directoris and their descendants
|
||||||
|
const segments = path[1].split(posix.sep) || [];
|
||||||
|
if (
|
||||||
|
segments.some((segment) => Create.do_not_sync_files.includes(segment))
|
||||||
|
) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip when the filename is a hidden file (starts with `.`)
|
||||||
|
if (segments[segments.length - 1].indexOf('.') === 0) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...prev, [path[1]]: curr };
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUrisMap(uri: URI) {
|
||||||
|
const basepath = uri.toString();
|
||||||
|
const exists = await this.fileService.exists(uri);
|
||||||
|
const uris =
|
||||||
|
(exists && this.URIsToMap(await this.recursiveURIs(uri), basepath)) || {};
|
||||||
|
return uris;
|
||||||
|
}
|
||||||
|
|
||||||
|
async treeDiff(source: URI, dest: URI): Promise<FilesToSync> {
|
||||||
|
const [sourceURIs, destURIs] = await Promise.all([
|
||||||
|
this.getUrisMap(source),
|
||||||
|
this.getUrisMap(dest),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const destBase = dest.toString();
|
||||||
|
const filesToWrite: FilesToWrite[] = [];
|
||||||
|
|
||||||
|
Object.keys(sourceURIs).forEach((path) => {
|
||||||
|
const destUri = destURIs[path] || new URI(destBase + path);
|
||||||
|
|
||||||
|
filesToWrite.push({ source: sourceURIs[path], dest: destUri });
|
||||||
|
delete destURIs[path];
|
||||||
|
});
|
||||||
|
|
||||||
|
const filesToDelete = Object.values(destURIs);
|
||||||
|
|
||||||
|
return { filesToWrite, filesToDelete };
|
||||||
|
}
|
||||||
|
|
||||||
async refresh(
|
async refresh(
|
||||||
node?: CompositeTreeNode
|
node?: CompositeTreeNode
|
||||||
): Promise<CompositeTreeNode | undefined> {
|
): Promise<CompositeTreeNode | undefined> {
|
||||||
@ -266,6 +327,25 @@ export class CloudSketchbookTree extends SketchbookTree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sync(source: URI, dest: URI) {
|
||||||
|
const { filesToWrite, filesToDelete } = await this.treeDiff(source, dest);
|
||||||
|
await Promise.all(
|
||||||
|
filesToWrite.map(async ({ source, dest }) => {
|
||||||
|
if ((await this.fileService.resolve(source)).isFile) {
|
||||||
|
const content = await this.fileService.read(source);
|
||||||
|
return this.fileService.write(dest, content.value);
|
||||||
|
}
|
||||||
|
return this.fileService.createFolder(dest);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
filesToDelete.map((file) =>
|
||||||
|
this.fileService.delete(file, { recursive: true })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
|
async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
|
||||||
return (await super.resolveChildren(parent)).sort((a, b) => {
|
return (await super.resolveChildren(parent)).sort((a, b) => {
|
||||||
if (
|
if (
|
||||||
@ -295,7 +375,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve fileStats for the given node, merging the local and remote childrens
|
* Retrieve fileStats for the given node, merging the local and remote childrens
|
||||||
* Local children take prevedence over remote ones
|
* Local children take precedence over remote ones
|
||||||
* @param node
|
* @param node
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
@ -376,6 +456,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
|||||||
const node = this.getNode(id);
|
const node = this.getNode(id);
|
||||||
if (fileStat.isDirectory) {
|
if (fileStat.isDirectory) {
|
||||||
if (DirNode.is(node)) {
|
if (DirNode.is(node)) {
|
||||||
|
node.uri = uri;
|
||||||
node.fileStat = fileStat;
|
node.fileStat = fileStat;
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
@ -391,6 +472,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
|||||||
}
|
}
|
||||||
if (FileNode.is(node)) {
|
if (FileNode.is(node)) {
|
||||||
node.fileStat = fileStat;
|
node.fileStat = fileStat;
|
||||||
|
node.uri = uri;
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
return <FileNode>{
|
return <FileNode>{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user