Skip to content

Commit 6515273

Browse files
Alberto Iannacconefstasi
Alberto Iannaccone
andauthored
[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>
1 parent 57b9eb9 commit 6515273

File tree

5 files changed

+162
-66
lines changed

5 files changed

+162
-66
lines changed

Diff for: arduino-ide-extension/src/browser/create/create-api.ts

+22-10
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,11 @@ export class CreateApi {
9393

9494
async readDirectory(
9595
posixPath: string,
96-
options: { recursive?: boolean; match?: string } = {}
96+
options: {
97+
recursive?: boolean;
98+
match?: string;
99+
skipSketchCache?: boolean;
100+
} = {}
97101
): Promise<Create.Resource[]> {
98102
const url = new URL(
99103
`${this.domain()}/files/d/$HOME/sketches_v2${posixPath}`
@@ -106,21 +110,29 @@ export class CreateApi {
106110
}
107111
const headers = await this.headers();
108112

109-
return this.run<Create.RawResource[]>(url, {
110-
method: 'GET',
111-
headers,
112-
})
113-
.then(async (result) => {
114-
// add arduino_secrets.h to the results, when reading a sketch main folder
115-
if (posixPath.length && posixPath !== posix.sep) {
116-
const sketch = this.sketchCache.getSketch(posixPath);
113+
const cachedSketch = this.sketchCache.getSketch(posixPath);
117114

115+
const sketchPromise = options.skipSketchCache
116+
? (cachedSketch && this.sketch(cachedSketch.id)) || Promise.resolve(null)
117+
: Promise.resolve(this.sketchCache.getSketch(posixPath));
118+
119+
return Promise.all([
120+
sketchPromise,
121+
this.run<Create.RawResource[]>(url, {
122+
method: 'GET',
123+
headers,
124+
}),
125+
])
126+
.then(async ([sketch, result]) => {
127+
if (posixPath.length && posixPath !== posix.sep) {
118128
if (sketch && sketch.secrets && sketch.secrets.length > 0) {
119129
result.push(this.getSketchSecretStat(sketch));
120130
}
121131
}
122132

123-
return result;
133+
return result.filter(
134+
(res) => !Create.do_not_sync_files.includes(res.name)
135+
);
124136
})
125137
.catch((reason) => {
126138
if (reason?.status === 404) return [] as Create.Resource[];

Diff for: arduino-ide-extension/src/browser/create/create-fs-provider.ts

+1-7
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ import { SketchesService } from '../../common/protocol';
3030
import { ArduinoPreferences } from '../arduino-preferences';
3131
import { Create } from './typings';
3232

33-
export const REMOTE_ONLY_FILES = ['sketch.json'];
34-
3533
@injectable()
3634
export class CreateFsProvider
3735
implements
@@ -109,14 +107,10 @@ export class CreateFsProvider
109107
const resources = await this.getCreateApi.readDirectory(
110108
uri.path.toString()
111109
);
112-
return resources
113-
.filter((res) => !REMOTE_ONLY_FILES.includes(res.name))
114-
.map(({ name, type }) => [name, this.toFileType(type)]);
110+
return resources.map(({ name, type }) => [name, this.toFileType(type)]);
115111
}
116112

117113
async delete(uri: URI, opts: FileDeleteOptions): Promise<void> {
118-
return;
119-
120114
if (!opts.recursive) {
121115
throw new Error(
122116
'Arduino Create file-system provider does not support non-recursive deletion.'

Diff for: arduino-ide-extension/src/browser/create/typings.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export namespace Create {
2121

2222
export type ResourceType = 'sketch' | 'folder' | 'file';
2323
export const arduino_secrets_file = 'arduino_secrets.h';
24-
export const do_not_sync_files = ['.theia'];
24+
export const do_not_sync_files = ['.theia', 'sketch.json'];
2525
export interface Resource {
2626
readonly name: string;
2727
/**

Diff for: arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketch-cache.ts

+8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ export class SketchCache {
2222
return this.fileStats[path] || null;
2323
}
2424

25+
purgeByPath(path: string): void {
26+
for (const itemPath in this.fileStats) {
27+
if (itemPath.indexOf(path) === 0) {
28+
delete this.fileStats[itemPath];
29+
}
30+
}
31+
}
32+
2533
addSketch(sketch: Create.Sketch): void {
2634
const { path } = sketch;
2735
const posixPath = toPosixPath(path);

Diff for: arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree.ts

+130-48
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
PreferenceScope,
1818
} from '@theia/core/lib/browser/preferences/preference-service';
1919
import { MessageService } from '@theia/core/lib/common/message-service';
20-
import { REMOTE_ONLY_FILES } from './../../create/create-fs-provider';
2120
import { CreateApi } from '../../create/create-api';
2221
import { CreateUri } from '../../create/create-uri';
2322
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
@@ -33,10 +32,17 @@ import { ArduinoPreferences } from '../../arduino-preferences';
3332
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
3433
import { FileStat } from '@theia/filesystem/lib/common/files';
3534
import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree';
35+
import { posix, splitSketchPath } from '../../create/create-paths';
36+
import { Create } from '../../create/typings';
3637

3738
const MESSAGE_TIMEOUT = 5 * 1000;
3839
const deepmerge = require('deepmerge').default;
3940

41+
type FilesToWrite = { source: URI; dest: URI };
42+
type FilesToSync = {
43+
filesToWrite: FilesToWrite[];
44+
filesToDelete: URI[];
45+
};
4046
@injectable()
4147
export class CloudSketchbookTree extends SketchbookTree {
4248
@inject(FileService)
@@ -94,7 +100,7 @@ export class CloudSketchbookTree extends SketchbookTree {
94100

95101
async pull(arg: any): Promise<void> {
96102
const {
97-
model,
103+
// model,
98104
node,
99105
}: {
100106
model: CloudSketchbookTreeModel;
@@ -127,47 +133,12 @@ export class CloudSketchbookTree extends SketchbookTree {
127133
const commandsCopy = node.commands;
128134
node.commands = [];
129135

130-
// check if the sketch dir already exist
131-
if (CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
132-
const filesToPull = (
133-
await this.createApi.readDirectory(node.remoteUri.path.toString())
134-
).filter((file: any) => !REMOTE_ONLY_FILES.includes(file.name));
135-
136-
await Promise.all(
137-
filesToPull.map((file: any) => {
138-
const uri = CreateUri.toUri(file);
139-
this.fileService.copy(uri, LocalCacheUri.root.resolve(uri.path), {
140-
overwrite: true,
141-
});
142-
})
143-
);
136+
const localUri = await this.fileService.toUnderlyingResource(
137+
LocalCacheUri.root.resolve(node.remoteUri.path)
138+
);
139+
await this.sync(node.remoteUri, localUri);
144140

145-
// open the pulled files in the current workspace
146-
const currentSketch = await this.sketchServiceClient.currentSketch();
147-
148-
if (
149-
!CreateUri.is(node.uri) &&
150-
currentSketch &&
151-
currentSketch.uri === node.uri.toString()
152-
) {
153-
filesToPull.forEach(async (file) => {
154-
const localUri = LocalCacheUri.root.resolve(
155-
CreateUri.toUri(file).path
156-
);
157-
const underlying = await this.fileService.toUnderlyingResource(
158-
localUri
159-
);
160-
161-
model.open(underlying);
162-
});
163-
}
164-
} else {
165-
await this.fileService.copy(
166-
node.remoteUri,
167-
LocalCacheUri.root.resolve(node.uri.path),
168-
{ overwrite: true }
169-
);
170-
}
141+
this.sketchCache.purgeByPath(node.remoteUri.path.toString());
171142

172143
node.commands = commandsCopy;
173144
this.messageService.info(`Done pulling ‘${node.fileStat.name}’.`, {
@@ -214,17 +185,107 @@ export class CloudSketchbookTree extends SketchbookTree {
214185
}
215186
const commandsCopy = node.commands;
216187
node.commands = [];
217-
// delete every first level file, then push everything
218-
const result = await this.fileService.copy(node.uri, node.remoteUri, {
219-
overwrite: true,
220-
});
188+
189+
const localUri = await this.fileService.toUnderlyingResource(
190+
LocalCacheUri.root.resolve(node.remoteUri.path)
191+
);
192+
await this.sync(localUri, node.remoteUri);
193+
194+
this.sketchCache.purgeByPath(node.remoteUri.path.toString());
195+
221196
node.commands = commandsCopy;
222-
this.messageService.info(`Done pushing ‘${result.name}’.`, {
197+
this.messageService.info(`Done pushing ‘${node.fileStat.name}’.`, {
223198
timeout: MESSAGE_TIMEOUT,
224199
});
225200
});
226201
}
227202

203+
async recursiveURIs(uri: URI): Promise<URI[]> {
204+
// remote resources can be fetched one-shot via api
205+
if (CreateUri.is(uri)) {
206+
const resources = await this.createApi.readDirectory(
207+
uri.path.toString(),
208+
{ recursive: true, skipSketchCache: true }
209+
);
210+
return resources.map((resource) =>
211+
CreateUri.toUri(splitSketchPath(resource.path)[1])
212+
);
213+
}
214+
215+
const fileStat = await this.fileService.resolve(uri, {
216+
resolveMetadata: false,
217+
});
218+
219+
if (!fileStat.children || !fileStat.isDirectory) {
220+
return [fileStat.resource];
221+
}
222+
223+
let childrenUris: URI[] = [];
224+
225+
for await (const child of fileStat.children) {
226+
childrenUris = [
227+
...childrenUris,
228+
...(await this.recursiveURIs(child.resource)),
229+
];
230+
}
231+
232+
return [fileStat.resource, ...childrenUris];
233+
}
234+
235+
private URIsToMap(uris: URI[], basepath: string): Record<string, URI> {
236+
return uris.reduce((prev: Record<string, URI>, curr) => {
237+
const path = curr.toString().split(basepath);
238+
239+
if (path.length !== 2 || path[1].length === 0) {
240+
return prev;
241+
}
242+
243+
// do not map "do_not_sync" files/directoris and their descendants
244+
const segments = path[1].split(posix.sep) || [];
245+
if (
246+
segments.some((segment) => Create.do_not_sync_files.includes(segment))
247+
) {
248+
return prev;
249+
}
250+
251+
// skip when the filename is a hidden file (starts with `.`)
252+
if (segments[segments.length - 1].indexOf('.') === 0) {
253+
return prev;
254+
}
255+
256+
return { ...prev, [path[1]]: curr };
257+
}, {});
258+
}
259+
260+
async getUrisMap(uri: URI) {
261+
const basepath = uri.toString();
262+
const exists = await this.fileService.exists(uri);
263+
const uris =
264+
(exists && this.URIsToMap(await this.recursiveURIs(uri), basepath)) || {};
265+
return uris;
266+
}
267+
268+
async treeDiff(source: URI, dest: URI): Promise<FilesToSync> {
269+
const [sourceURIs, destURIs] = await Promise.all([
270+
this.getUrisMap(source),
271+
this.getUrisMap(dest),
272+
]);
273+
274+
const destBase = dest.toString();
275+
const filesToWrite: FilesToWrite[] = [];
276+
277+
Object.keys(sourceURIs).forEach((path) => {
278+
const destUri = destURIs[path] || new URI(destBase + path);
279+
280+
filesToWrite.push({ source: sourceURIs[path], dest: destUri });
281+
delete destURIs[path];
282+
});
283+
284+
const filesToDelete = Object.values(destURIs);
285+
286+
return { filesToWrite, filesToDelete };
287+
}
288+
228289
async refresh(
229290
node?: CompositeTreeNode
230291
): Promise<CompositeTreeNode | undefined> {
@@ -266,6 +327,25 @@ export class CloudSketchbookTree extends SketchbookTree {
266327
}
267328
}
268329

330+
async sync(source: URI, dest: URI) {
331+
const { filesToWrite, filesToDelete } = await this.treeDiff(source, dest);
332+
await Promise.all(
333+
filesToWrite.map(async ({ source, dest }) => {
334+
if ((await this.fileService.resolve(source)).isFile) {
335+
const content = await this.fileService.read(source);
336+
return this.fileService.write(dest, content.value);
337+
}
338+
return this.fileService.createFolder(dest);
339+
})
340+
);
341+
342+
await Promise.all(
343+
filesToDelete.map((file) =>
344+
this.fileService.delete(file, { recursive: true })
345+
)
346+
);
347+
}
348+
269349
async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
270350
return (await super.resolveChildren(parent)).sort((a, b) => {
271351
if (
@@ -295,7 +375,7 @@ export class CloudSketchbookTree extends SketchbookTree {
295375

296376
/**
297377
* Retrieve fileStats for the given node, merging the local and remote childrens
298-
* Local children take prevedence over remote ones
378+
* Local children take precedence over remote ones
299379
* @param node
300380
* @returns
301381
*/
@@ -376,6 +456,7 @@ export class CloudSketchbookTree extends SketchbookTree {
376456
const node = this.getNode(id);
377457
if (fileStat.isDirectory) {
378458
if (DirNode.is(node)) {
459+
node.uri = uri;
379460
node.fileStat = fileStat;
380461
return node;
381462
}
@@ -391,6 +472,7 @@ export class CloudSketchbookTree extends SketchbookTree {
391472
}
392473
if (FileNode.is(node)) {
393474
node.fileStat = fileStat;
475+
node.uri = uri;
394476
return node;
395477
}
396478
return <FileNode>{

0 commit comments

Comments
 (0)